├── system.properties ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── build.yml │ └── pr-build.yml ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── src ├── main │ ├── java │ │ └── app │ │ │ └── fitbuddy │ │ │ ├── exception │ │ │ ├── FitBuddyException.java │ │ │ └── ControllerExceptionHandler.java │ │ │ ├── repository │ │ │ ├── DefaultExerciseRepository.java │ │ │ ├── RoleRepository.java │ │ │ ├── HistoryRepository.java │ │ │ ├── AppUserRepository.java │ │ │ └── ExerciseRepository.java │ │ │ ├── dto │ │ │ ├── accountinfo │ │ │ │ ├── AccountInfoResponseDTO.java │ │ │ │ └── AccountInfoUpdateDTO.java │ │ │ ├── role │ │ │ │ ├── RoleResponseDTO.java │ │ │ │ ├── RoleUpdateDTO.java │ │ │ │ └── RoleRequestDTO.java │ │ │ ├── exercise │ │ │ │ ├── ExerciseResponseDTO.java │ │ │ │ ├── ExerciseUpdateDTO.java │ │ │ │ └── ExerciseRequestDTO.java │ │ │ ├── LoginDTO.java │ │ │ ├── RegisterDTO.java │ │ │ ├── history │ │ │ │ ├── HistoryResponseDTO.java │ │ │ │ ├── HistoryUpdateDTO.java │ │ │ │ └── HistoryRequestDTO.java │ │ │ └── appuser │ │ │ │ ├── AppUserResponseDTO.java │ │ │ │ ├── AppUserRequestDTO.java │ │ │ │ └── AppUserUpdateDTO.java │ │ │ ├── service │ │ │ ├── crud │ │ │ │ ├── CrudService.java │ │ │ │ ├── HistoryCrudService.java │ │ │ │ ├── RoleCrudService.java │ │ │ │ ├── AppUserCrudService.java │ │ │ │ └── ExerciseCrudService.java │ │ │ ├── mapper │ │ │ │ ├── MapperService.java │ │ │ │ ├── RoleMapperService.java │ │ │ │ ├── ExerciseMapperService.java │ │ │ │ ├── AppUserMapperService.java │ │ │ │ └── HistoryMapperService.java │ │ │ └── operation │ │ │ │ ├── NewUserService.java │ │ │ │ ├── AccountInfoService.java │ │ │ │ ├── RegisterService.java │ │ │ │ └── LoginService.java │ │ │ ├── FitBuddyApplication.java │ │ │ ├── controller │ │ │ ├── operation │ │ │ │ ├── WebController.java │ │ │ │ ├── LoginController.java │ │ │ │ ├── RegisterController.java │ │ │ │ └── AccountInfoController.java │ │ │ └── crud │ │ │ │ ├── AppUserController.java │ │ │ │ ├── ExerciseController.java │ │ │ │ └── HistoryController.java │ │ │ ├── entity │ │ │ ├── Role.java │ │ │ ├── DefaultExercise.java │ │ │ ├── Exercise.java │ │ │ ├── AppUser.java │ │ │ └── History.java │ │ │ ├── annotation │ │ │ ├── FitBuddyDate.java │ │ │ └── validator │ │ │ │ └── FitBuddyDateValidator.java │ │ │ ├── security │ │ │ └── AppUserPrincipal.java │ │ │ └── config │ │ │ └── SecurityConfig.java │ └── resources │ │ ├── static │ │ ├── public │ │ │ ├── 403.html │ │ │ ├── js │ │ │ │ ├── commons.js │ │ │ │ ├── login.js │ │ │ │ └── register.js │ │ │ ├── login.html │ │ │ └── register.html │ │ └── user │ │ │ ├── css │ │ │ └── home.css │ │ │ ├── js │ │ │ ├── home.js │ │ │ ├── account.js │ │ │ ├── exercise.js │ │ │ └── history.js │ │ │ └── home.html │ │ ├── application.properties │ │ ├── data.sql │ │ └── schema.sql └── test │ └── java │ └── app │ └── fitbuddy │ ├── testhelper │ ├── annotation │ │ ├── WithMockAppUserPrincipal.java │ │ └── WithMockAppUserPrincipalSecurityContextFactory.java │ ├── RoleTestHelper.java │ ├── DefaultExerciseTestHelper.java │ ├── ExerciseTestHelper.java │ ├── AppUserTestHelper.java │ └── HistoryTestHelper.java │ ├── service │ ├── operation │ │ ├── LoginServiceTest.java │ │ ├── RegisterServiceTest.java │ │ └── NewUserServiceTest.java │ ├── crud │ │ ├── HistoryCrudServiceTest.java │ │ ├── RoleCrudServiceTest.java │ │ ├── ExerciseCrudServiceTest.java │ │ └── AppUserCrudServiceTest.java │ └── mapper │ │ ├── RoleMapperServiceTest.java │ │ ├── ExerciseMapperServiceTest.java │ │ ├── AppUserMapperServiceTest.java │ │ └── HistoryMapperServiceTest.java │ └── controller │ ├── operation │ ├── LoginControllerTest.java │ └── RegisterControllerTest.java │ └── crud │ └── ExerciseControllerTest.java ├── .gitignore ├── LICENSE.md ├── pom.xml ├── CONTRIBUTING.md ├── .all-contributorsrc ├── mvnw.cmd └── README.md /system.properties: -------------------------------------------------------------------------------- 1 | java.runtime.version=11 -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | ## Close issue(s) 4 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar 3 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/exception/FitBuddyException.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.exception; 2 | 3 | public class FitBuddyException extends RuntimeException { 4 | 5 | public FitBuddyException(String errorMessage) { 6 | super(errorMessage); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/static/public/403.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Access denied 7 | 8 | 9 |

Access denied

10 | 11 | 12 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/repository/DefaultExerciseRepository.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.repository; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | 5 | import app.fitbuddy.entity.DefaultExercise; 6 | 7 | public interface DefaultExerciseRepository extends CrudRepository { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/resources/static/user/css/home.css: -------------------------------------------------------------------------------- 1 | .icon-red { 2 | color: red; 3 | } 4 | 5 | .icon-grey { 6 | color: grey; 7 | } 8 | 9 | .icon-green { 10 | color: green; 11 | } 12 | 13 | .icon-orange { 14 | color: orange; 15 | } 16 | 17 | @media (max-width: 1080px) { 18 | .navbar-collapse 19 | { 20 | text-align: center; 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/dto/accountinfo/AccountInfoResponseDTO.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.dto.accountinfo; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Data 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | public class AccountInfoResponseDTO { 11 | 12 | private String name; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/dto/role/RoleResponseDTO.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.dto.role; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Data 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | public class RoleResponseDTO { 11 | 12 | private Integer id; 13 | private String name; 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/service/crud/CrudService.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.service.crud; 2 | 3 | public interface CrudService { 4 | 5 | public ResponseDTO create(RequestDTO requestDTO); 6 | public ResponseDTO readById(Integer id); 7 | public ResponseDTO update(Integer id, UpdateDTO updateDTO); 8 | public void delete(Integer id); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/dto/exercise/ExerciseResponseDTO.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.dto.exercise; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Data 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | public class ExerciseResponseDTO { 11 | 12 | private Integer id; 13 | private String name; 14 | private Integer appUserId; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/dto/LoginDTO.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.dto; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | import javax.validation.constraints.Size; 5 | 6 | import lombok.Data; 7 | 8 | @Data 9 | public class LoginDTO { 10 | 11 | @NotBlank 12 | @Size(min = 4, max = 15) 13 | private final String name; 14 | 15 | @NotBlank 16 | @Size(min = 4, max = 15) 17 | private final String password; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/dto/RegisterDTO.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.dto; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | import javax.validation.constraints.Size; 5 | 6 | import lombok.Data; 7 | 8 | @Data 9 | public class RegisterDTO { 10 | 11 | @NotBlank 12 | @Size(min = 4, max = 15) 13 | private final String name; 14 | 15 | @NotBlank 16 | @Size(min = 4, max = 15) 17 | private final String password; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/dto/role/RoleUpdateDTO.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.dto.role; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | import javax.validation.constraints.Size; 5 | 6 | import lombok.AllArgsConstructor; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | @Data 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | public class RoleUpdateDTO { 14 | 15 | @NotBlank 16 | @Size(min = 4, max = 16) 17 | private String name; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/dto/role/RoleRequestDTO.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.dto.role; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | import javax.validation.constraints.Size; 5 | 6 | import lombok.AllArgsConstructor; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | @Data 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | public class RoleRequestDTO { 14 | 15 | @NotBlank 16 | @Size(min = 4, max = 16) 17 | private String name; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/dto/exercise/ExerciseUpdateDTO.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.dto.exercise; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | import javax.validation.constraints.Size; 5 | 6 | import lombok.AllArgsConstructor; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | @Data 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | public class ExerciseUpdateDTO { 14 | 15 | @NotBlank 16 | @Size(min = 1, max = 32) 17 | private String name; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/dto/history/HistoryResponseDTO.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.dto.history; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Data 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | public class HistoryResponseDTO { 11 | 12 | private Integer id; 13 | private Integer appUserId; 14 | private String exerciseName; 15 | private Integer weight; 16 | private Integer reps; 17 | private String createdOn; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/service/mapper/MapperService.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.service.mapper; 2 | 3 | import java.util.List; 4 | 5 | public interface MapperService { 6 | 7 | public Entity requestDtoToEntity(RequestDTO requestDTO); 8 | public ResponseDTO entityToResponseDto(Entity entity); 9 | public List entitiesToResponseDtos(List entities); 10 | public Entity applyUpdateDtoToEntity(Entity entity, UpdateDTO updateDTO); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/repository/RoleRepository.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.repository; 2 | 3 | import java.util.Optional; 4 | 5 | import org.springframework.data.jpa.repository.Query; 6 | import org.springframework.data.repository.CrudRepository; 7 | 8 | import app.fitbuddy.entity.Role; 9 | 10 | public interface RoleRepository extends CrudRepository { 11 | 12 | @Query(value = "SELECT * FROM role WHERE role.name = ?1", nativeQuery = true) 13 | Optional findByName(String name); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/dto/appuser/AppUserResponseDTO.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.dto.appuser; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | 5 | import lombok.AllArgsConstructor; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Data 10 | @AllArgsConstructor 11 | @NoArgsConstructor 12 | public class AppUserResponseDTO { 13 | 14 | private Integer id; 15 | private String name; 16 | 17 | @JsonIgnore 18 | private String password; 19 | 20 | private String rolename; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/dto/history/HistoryUpdateDTO.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.dto.history; 2 | 3 | import javax.validation.constraints.Positive; 4 | import javax.validation.constraints.PositiveOrZero; 5 | 6 | import lombok.AllArgsConstructor; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | @Data 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | public class HistoryUpdateDTO { 14 | 15 | @PositiveOrZero 16 | private Integer weight; 17 | 18 | @Positive 19 | private Integer reps; 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/FitBuddyApplication.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration; 6 | 7 | @SpringBootApplication(exclude = {UserDetailsServiceAutoConfiguration.class}) 8 | public class FitBuddyApplication { 9 | 10 | public static void main(String[] args) { 11 | SpringApplication.run(FitBuddyApplication.class, args); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/repository/HistoryRepository.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.repository; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.data.jpa.repository.Query; 6 | import org.springframework.data.repository.CrudRepository; 7 | 8 | import app.fitbuddy.entity.History; 9 | 10 | public interface HistoryRepository extends CrudRepository { 11 | 12 | @Query(value = "SELECT * FROM history WHERE app_user_id = ?1 AND created_on = ?2", nativeQuery = true) 13 | List findAllByUserIdAndDate(Integer userId, String date); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/dto/accountinfo/AccountInfoUpdateDTO.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.dto.accountinfo; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | import javax.validation.constraints.Size; 5 | 6 | import lombok.AllArgsConstructor; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | @Data 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | public class AccountInfoUpdateDTO { 14 | 15 | @NotBlank 16 | @Size(min = 4, max = 15) 17 | private String oldPassword; 18 | 19 | @NotBlank 20 | @Size(min = 4, max = 15) 21 | private String newPassword; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/dto/exercise/ExerciseRequestDTO.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.dto.exercise; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | import javax.validation.constraints.Size; 5 | 6 | import com.fasterxml.jackson.annotation.JsonIgnore; 7 | 8 | import lombok.AllArgsConstructor; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | 12 | @Data 13 | @AllArgsConstructor 14 | @NoArgsConstructor 15 | public class ExerciseRequestDTO { 16 | 17 | @NotBlank 18 | @Size(min = 1, max = 32) 19 | private String name; 20 | 21 | @JsonIgnore 22 | private Integer appUserId; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/repository/AppUserRepository.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.repository; 2 | 3 | import java.util.Optional; 4 | 5 | import org.springframework.data.jpa.repository.Query; 6 | import org.springframework.data.repository.CrudRepository; 7 | import org.springframework.stereotype.Repository; 8 | 9 | import app.fitbuddy.entity.AppUser; 10 | 11 | @Repository 12 | public interface AppUserRepository extends CrudRepository { 13 | 14 | @Query(value = "SELECT * FROM app_user WHERE app_user.name = ?1", nativeQuery = true) 15 | Optional findByName(String name); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/app/fitbuddy/testhelper/annotation/WithMockAppUserPrincipal.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.testhelper.annotation; 2 | 3 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 4 | 5 | import java.lang.annotation.Retention; 6 | 7 | import org.springframework.security.test.context.support.WithSecurityContext; 8 | 9 | @Retention(RUNTIME) 10 | @WithSecurityContext(factory = WithMockAppUserPrincipalSecurityContextFactory.class) 11 | public @interface WithMockAppUserPrincipal { 12 | 13 | int id() default 1; 14 | String name() default "user"; 15 | String password() default "password"; 16 | String authority() default "USER"; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/dto/appuser/AppUserRequestDTO.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.dto.appuser; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | import javax.validation.constraints.Size; 5 | 6 | import lombok.AllArgsConstructor; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | @Data 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | public class AppUserRequestDTO { 14 | 15 | @NotBlank 16 | @Size(min = 4, max = 15) 17 | private String name; 18 | 19 | @NotBlank 20 | @Size(min = 4, max = 15) 21 | private String password; 22 | 23 | @NotBlank 24 | @Size(min = 4, max = 16) 25 | private String rolename; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/controller/operation/WebController.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.controller.operation; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | 6 | /** 7 | * Maps the client's GET requests to a html page. 8 | */ 9 | @Controller 10 | public class WebController { 11 | 12 | @GetMapping("/") 13 | public String getHomePage() { 14 | return "user/home.html"; 15 | } 16 | 17 | @GetMapping("/login") 18 | public String getLoginPage() { 19 | return "public/login.html"; 20 | } 21 | 22 | @GetMapping("/register") 23 | public String getRegisterPage() { 24 | return "public/register.html"; 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/entity/Role.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.entity; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Entity; 5 | import javax.persistence.GeneratedValue; 6 | import javax.persistence.GenerationType; 7 | import javax.persistence.Id; 8 | import javax.persistence.Table; 9 | 10 | import lombok.Getter; 11 | import lombok.Setter; 12 | import lombok.ToString; 13 | 14 | @Setter 15 | @Getter 16 | @ToString 17 | @Entity 18 | @Table(name= "role") 19 | public class Role { 20 | 21 | @Id 22 | @GeneratedValue(strategy = GenerationType.IDENTITY) 23 | @Column(name = "id") 24 | private Integer id; 25 | 26 | @Column(name = "name") 27 | private String name; 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/entity/DefaultExercise.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.entity; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Entity; 5 | import javax.persistence.GeneratedValue; 6 | import javax.persistence.GenerationType; 7 | import javax.persistence.Id; 8 | import javax.persistence.Table; 9 | 10 | import lombok.Getter; 11 | import lombok.Setter; 12 | import lombok.ToString; 13 | 14 | @Getter 15 | @Setter 16 | @ToString 17 | @Entity 18 | @Table(name = "default_exercise") 19 | public class DefaultExercise { 20 | 21 | @Id 22 | @GeneratedValue(strategy = GenerationType.IDENTITY) 23 | @Column(name = "id") 24 | private Integer id; 25 | 26 | @Column(name = "name") 27 | private String name; 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/annotation/FitBuddyDate.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.annotation; 2 | 3 | import app.fitbuddy.annotation.validator.FitBuddyDateValidator; 4 | 5 | import javax.validation.Constraint; 6 | import javax.validation.Payload; 7 | 8 | import java.lang.annotation.ElementType; 9 | import java.lang.annotation.Retention; 10 | import java.lang.annotation.RetentionPolicy; 11 | import java.lang.annotation.Target; 12 | 13 | @Retention(RetentionPolicy.RUNTIME) 14 | @Target(ElementType.FIELD) 15 | @Constraint(validatedBy = FitBuddyDateValidator.class) 16 | public @interface FitBuddyDate { 17 | 18 | String message() default "{fitbuddy.validation.message.invaliddate}"; 19 | 20 | Class[] groups() default { }; 21 | 22 | Class[] payload() default { }; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/resources/static/user/js/home.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", () => { 2 | onLoaded(); 3 | onHistory(); 4 | }); 5 | 6 | function onLoaded() { 7 | console.log("Page loaded."); 8 | showUserName(); 9 | } 10 | 11 | function onLogout() { 12 | window.location = "/logout"; 13 | } 14 | 15 | function onExercises() { 16 | hideStatus(); 17 | hideDiv("History"); 18 | hideDiv("Account"); 19 | showDiv("Exercises"); 20 | showExercises(); 21 | } 22 | 23 | function onHistory() { 24 | hideStatus(); 25 | hideDiv("Exercises"); 26 | hideDiv("Account"); 27 | showDiv("History"); 28 | resetCalendar(); 29 | showHistory(); 30 | } 31 | 32 | function onAccount() { 33 | hideStatus(); 34 | hideDiv("History"); 35 | hideDiv("Exercises"); 36 | showDiv("Account"); 37 | showAccount(); 38 | } -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/repository/ExerciseRepository.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.repository; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | import org.springframework.data.jpa.repository.Query; 7 | import org.springframework.data.repository.CrudRepository; 8 | 9 | import app.fitbuddy.entity.Exercise; 10 | 11 | public interface ExerciseRepository extends CrudRepository { 12 | 13 | @Query(value = "SELECT * FROM exercise WHERE app_user_id = ?1", nativeQuery = true) 14 | List findAllByUserId(Integer userId); 15 | 16 | @Query(value = "SELECT * FROM exercise WHERE name = ?1 AND app_user_id = ?2", nativeQuery = true) 17 | Optional findByNameAndUserId(String name, Integer userId); 18 | 19 | Optional findByName(String name); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/resources/static/public/js/commons.js: -------------------------------------------------------------------------------- 1 | function hideStatus() { 2 | var statusMessage = document.getElementById("status-message"); 3 | statusMessage.hidden = true; 4 | } 5 | 6 | function showStatus(message) { 7 | var statusMessage = document.getElementById("status-message"); 8 | statusMessage.hidden = false; 9 | statusMessage.innerHTML = message; 10 | } 11 | 12 | function clearFormValue(formId, valueName) { 13 | document.forms[formId][valueName].value = ""; 14 | } 15 | 16 | function hideDiv(id) { 17 | document.getElementById(id).style = "display:none"; 18 | document.getElementById(id).classList.add("d-none"); 19 | } 20 | 21 | function showDiv(id) { 22 | document.getElementById(id).style = "display:block"; 23 | document.getElementById(id).classList.remove("d-none"); 24 | } -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/dto/appuser/AppUserUpdateDTO.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.dto.appuser; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | import javax.validation.constraints.Size; 5 | 6 | import com.fasterxml.jackson.annotation.JsonIgnore; 7 | 8 | import lombok.AllArgsConstructor; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | 12 | @Data 13 | @AllArgsConstructor 14 | @NoArgsConstructor 15 | public class AppUserUpdateDTO { 16 | 17 | @NotBlank 18 | @Size(min = 4, max = 15) 19 | private String name; 20 | 21 | @JsonIgnore 22 | @Size(min = 4, max = 15) 23 | private String password; 24 | 25 | @NotBlank 26 | @Size(min = 4, max = 16) 27 | private String rolename; 28 | 29 | public AppUserUpdateDTO(String name, String rolename) { 30 | this.name = name; 31 | this.rolename = rolename; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/entity/Exercise.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.entity; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Entity; 5 | import javax.persistence.GeneratedValue; 6 | import javax.persistence.GenerationType; 7 | import javax.persistence.Id; 8 | import javax.persistence.JoinColumn; 9 | import javax.persistence.ManyToOne; 10 | import javax.persistence.Table; 11 | 12 | import lombok.Getter; 13 | import lombok.Setter; 14 | import lombok.ToString; 15 | 16 | @Setter 17 | @Getter 18 | @ToString 19 | @Entity 20 | @Table(name = "exercise") 21 | public class Exercise { 22 | 23 | @Id 24 | @GeneratedValue(strategy = GenerationType.IDENTITY) 25 | @Column(name = "id") 26 | private Integer id; 27 | 28 | @Column(name = "name") 29 | private String name; 30 | 31 | @ManyToOne 32 | @JoinColumn(name = "app_user_id", referencedColumnName = "id") 33 | private AppUser appUser; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/dto/history/HistoryRequestDTO.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.dto.history; 2 | 3 | import javax.validation.constraints.NotBlank; 4 | import javax.validation.constraints.Positive; 5 | import javax.validation.constraints.PositiveOrZero; 6 | import javax.validation.constraints.Size; 7 | 8 | import app.fitbuddy.annotation.FitBuddyDate; 9 | 10 | import com.fasterxml.jackson.annotation.JsonIgnore; 11 | 12 | import lombok.AllArgsConstructor; 13 | import lombok.Data; 14 | import lombok.NoArgsConstructor; 15 | 16 | @Data 17 | @AllArgsConstructor 18 | @NoArgsConstructor 19 | public class HistoryRequestDTO { 20 | 21 | @JsonIgnore 22 | private Integer appUserId; 23 | 24 | @NotBlank 25 | @Size(min = 1, max = 32) 26 | private String exerciseName; 27 | 28 | @PositiveOrZero 29 | private Integer weight; 30 | 31 | @Positive 32 | private Integer reps; 33 | 34 | @FitBuddyDate 35 | private String createdOn; 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | ### Datasource config 2 | spring.datasource.url=jdbc:h2:mem:db 3 | spring.datasource.driverClassName=org.h2.Driver 4 | spring.datasource.username=admin 5 | spring.datasource.password=admin 6 | spring.datasource.initialization-mode=always 7 | 8 | ### SQL DEBUG 9 | spring.jpa.show-sql=false 10 | 11 | ### How Hibernate creates the schema 12 | spring.jpa.hibernate.ddl-auto=none 13 | ### If true, data.sql and schema.sql run after Hibernate 14 | spring.jpa.defer-datasource-initialization=false 15 | 16 | ### Enable the H2 console 17 | spring.h2.console.enabled=true 18 | ### Set the H2 console path e.g. localhost:8080/console 19 | spring.h2.console.path=/console 20 | 21 | ### Include the error message in response 22 | server.error.include-message=always 23 | 24 | ### Validation patterns 25 | fitbuddy.validation.pattern.date=yyyy-MM-dd 26 | 27 | ### Validation messages 28 | fitbuddy.validation.message.invaliddate=Invalid date -------------------------------------------------------------------------------- /src/main/resources/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO default_exercise 2 | (name) 3 | VALUES 4 | ('Flat Barbell Bench Press'), 5 | ('Incline Barbell Bench Press'), 6 | ('Flat Dumbbell Bench Press'), 7 | ('Incline Dumbbell Bench Press'); 8 | 9 | INSERT INTO role 10 | (name) 11 | VALUES 12 | ('ADMIN'), 13 | ('USER'); 14 | 15 | INSERT INTO app_user 16 | (name, password, role_id) 17 | VALUES 18 | ('admin', '$2a$12$v/mRrRfQ18gpz5ekVluQ8eEEUiN.7gNJ4MtxQCbmWWCavtagHl6cy', 01), 19 | ('user', '$2a$12$.gvecgyfihq.ozfUhC/pGe48uNmgzsYpluQabfwWWvlO4G5xsXQXy', 02); 20 | 21 | INSERT INTO exercise 22 | (name, app_user_id) 23 | SELECT name, '2' 24 | FROM default_exercise; 25 | 26 | INSERT INTO history 27 | (app_user_id, exercise_id, weight, reps, created_on) 28 | VALUES 29 | (2, 1, 100, 10, CURRENT_DATE()), 30 | (2, 1, 100, 10, CURRENT_DATE()), 31 | (2, 1, 100, 10, CURRENT_DATE()), 32 | (2, 2, 60, 10, CURRENT_DATE()), 33 | (2, 2, 60, 10, CURRENT_DATE()), 34 | (2, 2, 60, 10, CURRENT_DATE()); -------------------------------------------------------------------------------- /src/test/java/app/fitbuddy/testhelper/RoleTestHelper.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.testhelper; 2 | 3 | import app.fitbuddy.dto.role.RoleResponseDTO; 4 | import app.fitbuddy.entity.Role; 5 | 6 | public class RoleTestHelper { 7 | 8 | private static final Integer ID = 1; 9 | private static final String NAME = "roleName"; 10 | 11 | public static Role getMockRole() { 12 | return getMockRole(ID, NAME); 13 | } 14 | 15 | public static Role getMockRole(Integer id, String name) { 16 | Role role = new Role(); 17 | role.setId(id); 18 | role.setName(name); 19 | return role; 20 | } 21 | 22 | public static boolean isEqual(RoleResponseDTO roleResponseDTO, Role role) { 23 | return (roleResponseDTO.getId().equals(role.getId()) && 24 | roleResponseDTO.getName().equals(role.getName())); 25 | } 26 | 27 | public static boolean isEqual(Role role, RoleResponseDTO roleResponseDTO) { 28 | return isEqual(roleResponseDTO, role); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/entity/AppUser.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.entity; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Entity; 5 | import javax.persistence.GeneratedValue; 6 | import javax.persistence.GenerationType; 7 | import javax.persistence.Id; 8 | import javax.persistence.JoinColumn; 9 | import javax.persistence.ManyToOne; 10 | import javax.persistence.Table; 11 | 12 | import lombok.Getter; 13 | import lombok.Setter; 14 | import lombok.ToString; 15 | 16 | @Getter 17 | @Setter 18 | @ToString 19 | @Entity 20 | @Table(name = "app_user") 21 | public class AppUser { 22 | 23 | @Id 24 | @GeneratedValue(strategy = GenerationType.IDENTITY) 25 | @Column(name = "id") 26 | private Integer id; 27 | 28 | @Column(name = "name") 29 | private String name; 30 | 31 | @Column(name = "password") 32 | private String password; 33 | 34 | @ManyToOne 35 | @JoinColumn(name = "role_id", referencedColumnName = "id") 36 | private Role role; 37 | 38 | } 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | 3 | ### Binaries ### 4 | target/ 5 | bin/ 6 | 7 | ### Package Files ### 8 | *.jar 9 | *.war 10 | *.nar 11 | *.ear 12 | *.zip 13 | *.tar.gz 14 | *.rar 15 | 16 | ### Java ### 17 | *.class 18 | 19 | ### Log file ### 20 | *.log 21 | 22 | ### System Files ### 23 | .DS_Store 24 | Thumbs.db 25 | *.bak 26 | 27 | ### STS ### 28 | .apt_generated 29 | .classpath 30 | .factorypath 31 | .project 32 | .settings 33 | .springBeans 34 | .sts4-cache 35 | 36 | ### Maven ### 37 | target/ 38 | pom.xml.tag 39 | pom.xml.releaseBackup 40 | pom.xml.versionsBackup 41 | pom.xml.next 42 | release.properties 43 | dependency-reduced-pom.xml 44 | buildNumber.properties 45 | .mvn/timing.properties 46 | .mvn/wrapper/maven-wrapper.jar 47 | 48 | 49 | ### IntelliJ IDEA ### 50 | .idea 51 | *.iws 52 | *.iml 53 | *.ipr 54 | 55 | ### NetBeans ### 56 | /nbproject/private/ 57 | /nbbuild/ 58 | /dist/ 59 | /nbdist/ 60 | /.nb-gradle/ 61 | build/ 62 | !**/src/main/**/build/ 63 | !**/src/test/**/build/ 64 | 65 | ### VS Code ### 66 | .vscode/ 67 | 68 | ### Vi text editor 69 | *.swp 70 | -------------------------------------------------------------------------------- /src/main/resources/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE role ( 2 | id INT NOT NULL AUTO_INCREMENT, 3 | name VARCHAR(16) NOT NULL, 4 | PRIMARY KEY (id) 5 | ); 6 | 7 | CREATE TABLE app_user ( 8 | id INT NOT NULL AUTO_INCREMENT, 9 | name VARCHAR(16) NOT NULL, 10 | password VARCHAR(72) NOT NULL, 11 | role_id INT NOT NULL, 12 | PRIMARY KEY (id), 13 | FOREIGN KEY (role_id) REFERENCES role(id) 14 | ); 15 | 16 | CREATE TABLE exercise ( 17 | id INT NOT NULL AUTO_INCREMENT, 18 | name VARCHAR(32) NOT NULL, 19 | app_user_id INT NOT NULL, 20 | PRIMARY KEY (id), 21 | FOREIGN KEY (app_user_id) REFERENCES app_user(id) 22 | ); 23 | 24 | CREATE TABLE history ( 25 | id INT NOT NULL AUTO_INCREMENT, 26 | app_user_id INT NOT NULL, 27 | exercise_id INT NOT NULL, 28 | weight INT, 29 | reps INT NOT NULL, 30 | created_on VARCHAR(16) NOT NULL, 31 | PRIMARY KEY (id), 32 | FOREIGN KEY (app_user_id) REFERENCES app_user(id), 33 | FOREIGN KEY (exercise_id) REFERENCES exercise(id) 34 | ); 35 | 36 | CREATE TABLE default_exercise ( 37 | id INT NOT NULL AUTO_INCREMENT, 38 | name VARCHAR(32) NOT NULL, 39 | PRIMARY KEY (id) 40 | ); -------------------------------------------------------------------------------- /src/test/java/app/fitbuddy/testhelper/DefaultExerciseTestHelper.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.testhelper; 2 | 3 | import app.fitbuddy.dto.exercise.ExerciseRequestDTO; 4 | import app.fitbuddy.entity.DefaultExercise; 5 | 6 | public class DefaultExerciseTestHelper { 7 | 8 | private static final Integer ID = 1; 9 | private static final String NAME = "defaultExerciseName"; 10 | 11 | public static DefaultExercise getMockDefaultExercise() { 12 | return getMockDefaultExercise(ID, NAME); 13 | } 14 | 15 | public static DefaultExercise getMockDefaultExercise(Integer id, String name) { 16 | DefaultExercise defaultExercise = new DefaultExercise(); 17 | defaultExercise.setId(id); 18 | defaultExercise.setName(name); 19 | return defaultExercise; 20 | } 21 | 22 | public static boolean isEqual(ExerciseRequestDTO exerciseRequestDTO, DefaultExercise exercise) { 23 | return exerciseRequestDTO.getName().equals(exercise.getName()); 24 | } 25 | 26 | public static boolean isEqual(DefaultExercise exercise, ExerciseRequestDTO exerciseRequestDTO) { 27 | return isEqual(exerciseRequestDTO, exercise); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 mepox 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/entity/History.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.entity; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Entity; 5 | import javax.persistence.GeneratedValue; 6 | import javax.persistence.GenerationType; 7 | import javax.persistence.Id; 8 | import javax.persistence.JoinColumn; 9 | import javax.persistence.ManyToOne; 10 | import javax.persistence.Table; 11 | 12 | import lombok.Getter; 13 | import lombok.Setter; 14 | import lombok.ToString; 15 | 16 | @Setter 17 | @Getter 18 | @ToString 19 | @Entity 20 | @Table(name = "history") 21 | public class History { 22 | 23 | @Id 24 | @GeneratedValue(strategy = GenerationType.IDENTITY) 25 | @Column(name = "id") 26 | private Integer id; 27 | 28 | @ManyToOne 29 | @JoinColumn(name = "exercise_id", referencedColumnName = "id") 30 | private Exercise exercise; 31 | 32 | @Column(name = "weight") 33 | private Integer weight; 34 | 35 | @Column(name = "reps") 36 | private Integer reps; 37 | 38 | @Column(name = "created_on") 39 | private String createdOn; 40 | 41 | @ManyToOne 42 | @JoinColumn(name = "app_user_id", referencedColumnName = "id") 43 | private AppUser appUser; 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/controller/operation/LoginController.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.controller.operation; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.web.bind.annotation.PostMapping; 7 | import org.springframework.web.bind.annotation.RequestBody; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | import app.fitbuddy.dto.LoginDTO; 11 | import app.fitbuddy.service.operation.LoginService; 12 | 13 | import javax.validation.Valid; 14 | 15 | /** 16 | * Handles the client's REST API requests that are related to logging in. 17 | */ 18 | @RestController 19 | public class LoginController { 20 | 21 | private final Logger logger; 22 | private final LoginService loginService; 23 | 24 | @Autowired 25 | public LoginController(LoginService loginService) { 26 | this.loginService = loginService; 27 | this.logger = LoggerFactory.getLogger(LoginController.class); 28 | } 29 | 30 | @PostMapping("/login/perform_login") 31 | public void login(@Valid @RequestBody LoginDTO loginDTO) { 32 | logger.info("Trying to log in: {}", loginDTO); 33 | loginService.login(loginDTO); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/resources/static/public/js/login.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", () => { 2 | onLoaded(); 3 | }); 4 | 5 | function onLoaded() { 6 | clearLoginForm(); 7 | console.log("Page loaded."); 8 | } 9 | 10 | function onLogin() { 11 | let name = document.forms["loginForm"]["name"].value.trim(); 12 | let password = document.forms["loginForm"]["password"].value; 13 | 14 | name = name.trim(); 15 | password = password.trim(); 16 | 17 | let url = "/login/perform_login"; 18 | let data = { "name" : name, 19 | "password" : password }; 20 | 21 | let xhr = new XMLHttpRequest(); 22 | xhr.open("POST", url); 23 | xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); 24 | xhr.send(JSON.stringify(data)); 25 | 26 | xhr.onreadystatechange = function() { 27 | if (this.readyState == XMLHttpRequest.DONE) { 28 | if (this.status == 200) { 29 | // SUCCESS 30 | showStatus("Logged in successfully.") 31 | window.open("/", "_top"); 32 | } else { 33 | // ERROR 34 | showStatus(this.responseText); 35 | clearLoginForm(); 36 | } 37 | } 38 | } 39 | } 40 | 41 | function clearLoginForm() { 42 | clearFormValue("loginForm", "name"); 43 | clearFormValue("loginForm", "password"); 44 | } -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/annotation/validator/FitBuddyDateValidator.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.annotation.validator; 2 | 3 | import app.fitbuddy.annotation.FitBuddyDate; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.stereotype.Service; 8 | 9 | import javax.validation.ConstraintValidator; 10 | import javax.validation.ConstraintValidatorContext; 11 | 12 | import java.time.LocalDate; 13 | import java.time.format.DateTimeFormatter; 14 | import java.time.format.DateTimeParseException; 15 | 16 | @Service 17 | public class FitBuddyDateValidator implements ConstraintValidator { 18 | 19 | private final DateTimeFormatter dateTimeFormatter; 20 | 21 | @Autowired 22 | public FitBuddyDateValidator(@Value("${fitbuddy.validation.pattern.date:yyyy-MM-dd}") String datePattern) { 23 | dateTimeFormatter = DateTimeFormatter.ofPattern(datePattern); 24 | } 25 | 26 | @Override 27 | public boolean isValid(String value, ConstraintValidatorContext context) { 28 | try { 29 | LocalDate.parse(value, dateTimeFormatter); 30 | return true; 31 | } catch (DateTimeParseException parseException) { 32 | return false; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/app/fitbuddy/testhelper/ExerciseTestHelper.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.testhelper; 2 | 3 | import app.fitbuddy.dto.exercise.ExerciseResponseDTO; 4 | import app.fitbuddy.entity.AppUser; 5 | import app.fitbuddy.entity.Exercise; 6 | 7 | public class ExerciseTestHelper { 8 | 9 | private static final Integer ID = 1; 10 | private static final String NAME = "exerciseName"; 11 | 12 | public static Exercise getMockExercise() { 13 | return getMockExercise(ID, NAME, AppUserTestHelper.getMockAppUser()); 14 | } 15 | 16 | public static Exercise getMockExercise(Integer id, String name) { 17 | return getMockExercise(id, name, AppUserTestHelper.getMockAppUser()); 18 | } 19 | 20 | public static Exercise getMockExercise(Integer id, String name, AppUser appUser) { 21 | Exercise exercise = new Exercise(); 22 | exercise.setId(id); 23 | exercise.setName(name); 24 | exercise.setAppUser(appUser); 25 | return exercise; 26 | } 27 | 28 | public static boolean isEqual(ExerciseResponseDTO exerciseResponseDTO, Exercise exercise) { 29 | return (exerciseResponseDTO.getId().equals(exercise.getId()) && 30 | exerciseResponseDTO.getName().equals(exercise.getName()) && 31 | exerciseResponseDTO.getAppUserId().equals(exercise.getAppUser().getId())); 32 | } 33 | 34 | public static boolean isEqual(Exercise exercise, ExerciseResponseDTO exerciseResponseDTO) { 35 | return isEqual(exerciseResponseDTO, exercise); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/service/operation/NewUserService.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.service.operation; 2 | 3 | import app.fitbuddy.dto.exercise.ExerciseRequestDTO; 4 | import app.fitbuddy.entity.DefaultExercise; 5 | import app.fitbuddy.exception.FitBuddyException; 6 | import app.fitbuddy.repository.DefaultExerciseRepository; 7 | 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Service; 10 | 11 | import app.fitbuddy.service.crud.ExerciseCrudService; 12 | 13 | @Service 14 | public class NewUserService { 15 | 16 | private final DefaultExerciseRepository defaultExerciseRepository; 17 | private final ExerciseCrudService exerciseCrudService; 18 | 19 | @Autowired 20 | public NewUserService(DefaultExerciseRepository defaultExerciseRepository, ExerciseCrudService exerciseCrudService) { 21 | this.defaultExerciseRepository = defaultExerciseRepository; 22 | this.exerciseCrudService = exerciseCrudService; 23 | } 24 | 25 | public void addDefaultExercises(Integer appUserId) { 26 | if (appUserId == null || appUserId < 0) { 27 | throw new FitBuddyException("Internal server error - appUserId is not correct"); 28 | } 29 | Iterable defaultExercises = defaultExerciseRepository.findAll(); 30 | 31 | for (DefaultExercise defaultExercise : defaultExercises) { 32 | exerciseCrudService.create(new ExerciseRequestDTO(defaultExercise.getName(), appUserId)); 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/controller/operation/RegisterController.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.controller.operation; 2 | 3 | import javax.validation.Valid; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.web.bind.annotation.PostMapping; 9 | import org.springframework.web.bind.annotation.RequestBody; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | import app.fitbuddy.dto.RegisterDTO; 13 | import app.fitbuddy.service.operation.NewUserService; 14 | import app.fitbuddy.service.operation.RegisterService; 15 | 16 | @RestController 17 | public class RegisterController { 18 | 19 | private final Logger logger; 20 | private final RegisterService registerService; 21 | private final NewUserService newUserService; 22 | 23 | @Autowired 24 | public RegisterController(RegisterService registerService, NewUserService newUserService) { 25 | this.registerService = registerService; 26 | this.newUserService = newUserService; 27 | this.logger = LoggerFactory.getLogger(RegisterController.class); 28 | } 29 | 30 | @PostMapping("/register") 31 | public void register(@Valid @RequestBody RegisterDTO registerDTO) { 32 | logger.info("Trying to register with: {}", registerDTO); 33 | Integer appUserId = registerService.register(registerDTO.getName(), registerDTO.getPassword()); 34 | newUserService.addDefaultExercises(appUserId); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/security/AppUserPrincipal.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.security; 2 | 3 | import java.util.Collection; 4 | import java.util.List; 5 | 6 | import org.springframework.security.core.GrantedAuthority; 7 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 8 | import org.springframework.security.core.userdetails.UserDetails; 9 | 10 | import app.fitbuddy.entity.AppUser; 11 | 12 | public class AppUserPrincipal implements UserDetails { 13 | 14 | private static final long serialVersionUID = 1L; 15 | private final AppUser appUser; 16 | 17 | public AppUserPrincipal(AppUser appUser) { 18 | this.appUser = appUser; 19 | } 20 | 21 | @Override 22 | public Collection getAuthorities() { 23 | return List.of(new SimpleGrantedAuthority(appUser.getRole().getName())); 24 | } 25 | 26 | @Override 27 | public String getPassword() { 28 | return appUser.getPassword(); 29 | } 30 | 31 | @Override 32 | public String getUsername() { 33 | return appUser.getName(); 34 | } 35 | 36 | @Override 37 | public boolean isAccountNonExpired() { 38 | return true; 39 | } 40 | 41 | @Override 42 | public boolean isAccountNonLocked() { 43 | return true; 44 | } 45 | 46 | @Override 47 | public boolean isCredentialsNonExpired() { 48 | return true; 49 | } 50 | 51 | @Override 52 | public boolean isEnabled() { 53 | return true; 54 | } 55 | 56 | public Integer getId() { 57 | return appUser.getId(); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | name: Build and analyze 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 17 | 18 | - name: Set up JDK 11 19 | uses: actions/setup-java@v3 20 | with: 21 | java-version: '11' 22 | distribution: 'temurin' 23 | cache: maven 24 | 25 | - name: Cache SonarCloud packages 26 | uses: actions/cache@v3 27 | with: 28 | path: ~/.sonar/cache 29 | key: ${{ runner.os }}-sonar 30 | restore-keys: ${{ runner.os }}-sonar 31 | 32 | - name: Cache local Maven repository 33 | uses: actions/cache@v3 34 | with: 35 | path: ~/.m2/repository 36 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 37 | restore-keys: | 38 | ${{ runner.os }}-maven- 39 | 40 | - name: Build and analyze 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any 43 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 44 | run: | 45 | mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar \ 46 | -Dsonar.host.url=https://sonarcloud.io \ 47 | -Dsonar.organization=fitbuddy \ 48 | -Dsonar.projectKey=fitbuddy-app 49 | -------------------------------------------------------------------------------- /src/test/java/app/fitbuddy/testhelper/AppUserTestHelper.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.testhelper; 2 | 3 | import app.fitbuddy.dto.appuser.AppUserResponseDTO; 4 | import app.fitbuddy.entity.AppUser; 5 | import app.fitbuddy.entity.Role; 6 | 7 | public class AppUserTestHelper { 8 | 9 | private static final Integer ID = 1; 10 | private static final String NAME = "userName"; 11 | private static final String PASSWORD = "password"; 12 | 13 | public static AppUser getMockAppUser() { 14 | return getMockAppUser(ID, NAME, PASSWORD, RoleTestHelper.getMockRole()); 15 | } 16 | 17 | public static AppUser getMockAppUser(Integer id, String name, String password) { 18 | return getMockAppUser(id, name, password, RoleTestHelper.getMockRole()); 19 | } 20 | 21 | public static AppUser getMockAppUser(Integer id, String name, String password, Role role) { 22 | AppUser appUser = new AppUser(); 23 | appUser.setId(id); 24 | appUser.setName(name); 25 | appUser.setPassword(password); 26 | appUser.setRole(role); 27 | return appUser; 28 | } 29 | 30 | public static boolean isEqual(AppUserResponseDTO appUserResponseDTO, AppUser appUser) { 31 | return (appUserResponseDTO.getId().equals(appUser.getId()) && 32 | appUserResponseDTO.getName().equals(appUser.getName()) && 33 | appUserResponseDTO.getPassword().equals(appUser.getPassword()) && 34 | appUserResponseDTO.getRolename().equals(appUser.getRole().getName())); 35 | } 36 | 37 | public static boolean isEqual(AppUser appUser, AppUserResponseDTO appUserResponseDTO) { 38 | return isEqual(appUserResponseDTO, appUser); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/resources/static/public/js/register.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", () => { 2 | onLoaded(); 3 | }); 4 | 5 | function onLoaded() { 6 | clearRegisterForm(); 7 | console.log("Page loaded."); 8 | } 9 | 10 | function onRegister() { 11 | let name = document.forms["registerForm"]["name"].value; 12 | let password = document.forms["registerForm"]["password"].value; 13 | let passwordConfirm = document.forms["registerForm"]["passwordConfirm"].value; 14 | 15 | name = name.trim(); 16 | password = password.trim(); 17 | passwordConfirm = passwordConfirm.trim(); 18 | 19 | if (password !== passwordConfirm) { 20 | let message = "Password and confirm password doesn't match."; 21 | console.log("ERROR: " + message); 22 | showStatus(message); 23 | return; 24 | } 25 | 26 | let url = "/register"; 27 | let data = { name : name, 28 | password : password }; 29 | 30 | let xhr = new XMLHttpRequest(); 31 | xhr.open("POST", url); 32 | xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); 33 | xhr.send(JSON.stringify(data)); 34 | 35 | xhr.onreadystatechange = function() { 36 | if (this.readyState == XMLHttpRequest.DONE) { 37 | if (this.status == 200) { 38 | // SUCCESS 39 | showStatus("Registered successfully.") 40 | } else { 41 | // ERROR 42 | showStatus(this.responseText); 43 | } 44 | clearRegisterForm(); 45 | } 46 | } 47 | } 48 | 49 | function clearRegisterForm() { 50 | clearFormValue("registerForm", "name"); 51 | clearFormValue("registerForm", "password"); 52 | clearFormValue("registerForm", "passwordConfirm"); 53 | } -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/service/mapper/RoleMapperService.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.service.mapper; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | 7 | import org.springframework.stereotype.Service; 8 | 9 | import app.fitbuddy.dto.role.RoleRequestDTO; 10 | import app.fitbuddy.dto.role.RoleResponseDTO; 11 | import app.fitbuddy.dto.role.RoleUpdateDTO; 12 | import app.fitbuddy.entity.Role; 13 | 14 | @Service 15 | public class RoleMapperService implements MapperService { 16 | 17 | @Override 18 | public Role requestDtoToEntity(RoleRequestDTO requestDTO) { 19 | if (requestDTO == null) { 20 | return null; 21 | } 22 | Role role = new Role(); 23 | role.setName(requestDTO.getName()); 24 | return role; 25 | } 26 | 27 | @Override 28 | public RoleResponseDTO entityToResponseDto(Role entity) { 29 | if (entity == null) { 30 | return null; 31 | } 32 | return new RoleResponseDTO(entity.getId(), entity.getName()); 33 | } 34 | 35 | @Override 36 | public List entitiesToResponseDtos(List entities) { 37 | if (entities == null || entities.isEmpty()) { 38 | return Collections.emptyList(); 39 | } 40 | List result = new ArrayList<>(); 41 | for (Role entity : entities) { 42 | result.add(entityToResponseDto(entity)); 43 | } 44 | return result; 45 | } 46 | 47 | @Override 48 | public Role applyUpdateDtoToEntity(Role entity, RoleUpdateDTO updateDTO) { 49 | if (entity == null || updateDTO == null) { 50 | return null; 51 | } 52 | if (updateDTO.getName() != null) { 53 | entity.setName(updateDTO.getName()); 54 | } 55 | return entity; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/exception/ControllerExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.exception; 2 | 3 | import org.springframework.http.HttpHeaders; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.validation.FieldError; 7 | import org.springframework.web.bind.MethodArgumentNotValidException; 8 | import org.springframework.web.bind.annotation.ExceptionHandler; 9 | import org.springframework.web.bind.annotation.ResponseStatus; 10 | import org.springframework.web.bind.annotation.RestControllerAdvice; 11 | import org.springframework.web.context.request.WebRequest; 12 | import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; 13 | 14 | @RestControllerAdvice 15 | public class ControllerExceptionHandler extends ResponseEntityExceptionHandler { 16 | 17 | @Override 18 | protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, 19 | HttpHeaders headers, HttpStatus status, WebRequest request) { 20 | FieldError fe = ex.getBindingResult().getFieldError(); 21 | if (fe != null) { 22 | String field = fe.getField(); 23 | String message = fe.getDefaultMessage(); 24 | return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(field + " " + message); 25 | } else { 26 | return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Internal server error"); 27 | } 28 | 29 | 30 | } 31 | 32 | @ExceptionHandler(FitBuddyException.class) 33 | @ResponseStatus(HttpStatus.BAD_REQUEST) 34 | public ResponseEntity handleFitBuddyException(FitBuddyException ex) { 35 | return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage()); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/resources/static/public/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Login 8 | 9 | 10 |
11 |

FitBuddy

12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 |
27 | 30 | 31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/controller/operation/AccountInfoController.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.controller.operation; 2 | 3 | import javax.validation.Valid; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.security.access.prepost.PreAuthorize; 7 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.PutMapping; 10 | import org.springframework.web.bind.annotation.RequestBody; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RestController; 13 | 14 | import app.fitbuddy.dto.accountinfo.AccountInfoResponseDTO; 15 | import app.fitbuddy.dto.accountinfo.AccountInfoUpdateDTO; 16 | import app.fitbuddy.security.AppUserPrincipal; 17 | import app.fitbuddy.service.operation.AccountInfoService; 18 | 19 | @RestController 20 | @RequestMapping("/user/account") 21 | @PreAuthorize("authenticated") 22 | public class AccountInfoController { 23 | 24 | private final AccountInfoService accountInfoService; 25 | 26 | @Autowired 27 | public AccountInfoController(AccountInfoService accountInfoService) { 28 | this.accountInfoService = accountInfoService; 29 | } 30 | 31 | @GetMapping 32 | public AccountInfoResponseDTO read(@AuthenticationPrincipal AppUserPrincipal appUserPrincipal) { 33 | return accountInfoService.read(appUserPrincipal.getUsername()); 34 | } 35 | 36 | @PutMapping 37 | public void update(@RequestBody @Valid AccountInfoUpdateDTO accountInfoUpdateDTO, 38 | @AuthenticationPrincipal AppUserPrincipal appUserPrincipal) { 39 | accountInfoService.update(appUserPrincipal.getUsername(), accountInfoUpdateDTO); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/app/fitbuddy/service/operation/LoginServiceTest.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.service.operation; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertThrows; 4 | import static org.mockito.ArgumentMatchers.anyString; 5 | import static org.mockito.Mockito.when; 6 | 7 | import java.util.Optional; 8 | 9 | import app.fitbuddy.dto.LoginDTO; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | import org.mockito.InjectMocks; 13 | import org.mockito.Mock; 14 | import org.mockito.junit.jupiter.MockitoExtension; 15 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 16 | 17 | import app.fitbuddy.entity.AppUser; 18 | import app.fitbuddy.exception.FitBuddyException; 19 | import app.fitbuddy.repository.AppUserRepository; 20 | import app.fitbuddy.testhelper.AppUserTestHelper; 21 | 22 | @ExtendWith(MockitoExtension.class) 23 | class LoginServiceTest { 24 | 25 | @InjectMocks 26 | LoginService loginService; 27 | 28 | @Mock 29 | AppUserRepository appUserRepository; 30 | 31 | @Mock 32 | BCryptPasswordEncoder bCryptPasswordEncoder; 33 | 34 | @Test 35 | void usernameNotFound_throwFitBuddyException() { 36 | LoginDTO loginDtoMock = new LoginDTO("name", "password"); 37 | when(appUserRepository.findByName(anyString())).thenReturn(Optional.empty()); 38 | 39 | assertThrows(FitBuddyException.class, () -> loginService.login(loginDtoMock)); 40 | } 41 | 42 | @Test 43 | void passwordDoesntMatch_throwFitBuddyException() { 44 | AppUser appUser = AppUserTestHelper.getMockAppUser(); 45 | LoginDTO loginDtoMock = new LoginDTO("name", "incorrectPassword"); 46 | when(appUserRepository.findByName(anyString())).thenReturn(Optional.of(appUser)); 47 | when(bCryptPasswordEncoder.matches(anyString(), anyString())).thenReturn(false); 48 | 49 | assertThrows(FitBuddyException.class, () -> loginService.login(loginDtoMock)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/service/operation/AccountInfoService.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.service.operation; 2 | 3 | import javax.validation.Valid; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 7 | import org.springframework.stereotype.Service; 8 | 9 | import app.fitbuddy.dto.accountinfo.AccountInfoResponseDTO; 10 | import app.fitbuddy.dto.accountinfo.AccountInfoUpdateDTO; 11 | import app.fitbuddy.dto.appuser.AppUserResponseDTO; 12 | import app.fitbuddy.dto.appuser.AppUserUpdateDTO; 13 | import app.fitbuddy.exception.FitBuddyException; 14 | import app.fitbuddy.service.crud.AppUserCrudService; 15 | 16 | @Service 17 | public class AccountInfoService { 18 | 19 | private final AppUserCrudService appUserCrudService; 20 | private final BCryptPasswordEncoder bCryptPasswordEncoder; 21 | 22 | @Autowired 23 | public AccountInfoService(AppUserCrudService appUserCrudService, BCryptPasswordEncoder bCryptPasswordEncoder) { 24 | this.appUserCrudService = appUserCrudService; 25 | this.bCryptPasswordEncoder = bCryptPasswordEncoder; 26 | } 27 | 28 | public AccountInfoResponseDTO read(String name) { 29 | return new AccountInfoResponseDTO(appUserCrudService.readByName(name).getName()); 30 | } 31 | 32 | public void update(String name, @Valid AccountInfoUpdateDTO accountInfoUpdateDTO) { 33 | AppUserResponseDTO appUserResponseDTO = appUserCrudService.readByName(name); 34 | if (appUserResponseDTO == null) { 35 | throw new FitBuddyException("User not found with name: " + name); 36 | } 37 | if (!bCryptPasswordEncoder.matches(accountInfoUpdateDTO.getOldPassword(), appUserResponseDTO.getPassword())) { 38 | throw new FitBuddyException("Old password is not correct."); 39 | } 40 | appUserCrudService.update(appUserResponseDTO.getId(), new AppUserUpdateDTO(appUserResponseDTO.getName(), 41 | accountInfoUpdateDTO.getNewPassword(), appUserResponseDTO.getRolename())); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/resources/static/public/register.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Register 8 | 9 | 10 |
11 |

FitBuddy

12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 |
32 | Back to login 33 |
34 | 35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/test/java/app/fitbuddy/testhelper/annotation/WithMockAppUserPrincipalSecurityContextFactory.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.testhelper.annotation; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 6 | import org.springframework.security.core.Authentication; 7 | import org.springframework.security.core.GrantedAuthority; 8 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 9 | import org.springframework.security.core.context.SecurityContext; 10 | import org.springframework.security.core.context.SecurityContextHolder; 11 | import org.springframework.security.test.context.support.WithSecurityContextFactory; 12 | 13 | import app.fitbuddy.entity.AppUser; 14 | import app.fitbuddy.entity.Role; 15 | import app.fitbuddy.security.AppUserPrincipal; 16 | 17 | public class WithMockAppUserPrincipalSecurityContextFactory 18 | implements WithSecurityContextFactory { 19 | 20 | @Override 21 | public SecurityContext createSecurityContext(WithMockAppUserPrincipal withMockAppUserPrincipal) { 22 | SecurityContext context = SecurityContextHolder.createEmptyContext(); 23 | 24 | Role role = new Role(); 25 | role.setId(1); 26 | role.setName(withMockAppUserPrincipal.authority()); 27 | 28 | AppUser appUser = new AppUser(); 29 | appUser.setId(withMockAppUserPrincipal.id()); 30 | appUser.setName(withMockAppUserPrincipal.name()); 31 | appUser.setPassword(withMockAppUserPrincipal.password()); 32 | appUser.setRole(role); 33 | 34 | AppUserPrincipal principal = new AppUserPrincipal(appUser); 35 | 36 | List authorities = List.of( 37 | new SimpleGrantedAuthority(withMockAppUserPrincipal.authority())); 38 | 39 | Authentication authentication = new UsernamePasswordAuthenticationToken(principal, 40 | withMockAppUserPrincipal.password(), authorities); 41 | 42 | context.setAuthentication(authentication); 43 | 44 | return context; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/service/operation/RegisterService.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.service.operation; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Service; 7 | 8 | import app.fitbuddy.dto.appuser.AppUserRequestDTO; 9 | import app.fitbuddy.dto.appuser.AppUserResponseDTO; 10 | import app.fitbuddy.dto.role.RoleResponseDTO; 11 | import app.fitbuddy.exception.FitBuddyException; 12 | import app.fitbuddy.service.crud.AppUserCrudService; 13 | import app.fitbuddy.service.crud.RoleCrudService; 14 | 15 | /** 16 | * Provides a service to handle the registration process. 17 | */ 18 | @Service 19 | public class RegisterService { 20 | 21 | private final Logger logger; 22 | private final AppUserCrudService appUserCrudService; 23 | private final RoleCrudService roleCrudService; 24 | 25 | @Autowired 26 | public RegisterService(AppUserCrudService appUserCrudService, RoleCrudService roleCrudService) { 27 | this.appUserCrudService = appUserCrudService; 28 | this.roleCrudService = roleCrudService; 29 | this.logger = LoggerFactory.getLogger(RegisterService.class); 30 | } 31 | 32 | public Integer register(String name, String password) { 33 | // check if name already exists 34 | if (appUserCrudService.readByName(name) != null) { 35 | throw new FitBuddyException("Username already exists."); 36 | } 37 | 38 | // check if default user role exists 39 | RoleResponseDTO userRoleDto = roleCrudService.readByName("USER"); 40 | if (userRoleDto == null) { 41 | throw new FitBuddyException("Internal server error - default role doesn't exists."); 42 | } 43 | 44 | // create new app user 45 | AppUserRequestDTO appUserRequestDTO = new AppUserRequestDTO(name, password, userRoleDto.getName()); 46 | 47 | AppUserResponseDTO newAppUserDto = appUserCrudService.create(appUserRequestDTO); 48 | 49 | logger.info("User registered: {}", newAppUserDto); 50 | 51 | return newAppUserDto.getId(); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/app/fitbuddy/testhelper/HistoryTestHelper.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.testhelper; 2 | 3 | import app.fitbuddy.dto.history.HistoryResponseDTO; 4 | import app.fitbuddy.entity.AppUser; 5 | import app.fitbuddy.entity.Exercise; 6 | import app.fitbuddy.entity.History; 7 | 8 | public class HistoryTestHelper { 9 | 10 | private static final Integer ID = 1; 11 | private static final Integer WEIGHT = 11; 12 | private static final Integer REPS = 111; 13 | private static final String CREATED_ON = "01-01-2022"; 14 | 15 | public static History getMockHistory() { 16 | return getMockHistory(ID, WEIGHT, REPS, CREATED_ON, 17 | ExerciseTestHelper.getMockExercise(), AppUserTestHelper.getMockAppUser()); 18 | 19 | } 20 | 21 | public static History getMockHistory(Integer id, Integer weight, Integer reps, String createdOn) { 22 | return getMockHistory(id, weight, reps, createdOn, 23 | ExerciseTestHelper.getMockExercise(), AppUserTestHelper.getMockAppUser()); 24 | } 25 | 26 | public static History getMockHistory(Integer id, Integer weight, Integer reps, String createdOn, 27 | Exercise exercise, AppUser appUser) { 28 | History history = new History(); 29 | history.setId(id); 30 | history.setWeight(weight); 31 | history.setReps(reps); 32 | history.setCreatedOn(createdOn); 33 | history.setExercise(exercise); 34 | history.setAppUser(appUser); 35 | return history; 36 | } 37 | 38 | public static boolean isEqual(History history, HistoryResponseDTO historyResponseDTO) { 39 | return (history.getId().equals(historyResponseDTO.getId()) && 40 | history.getAppUser().getId().equals(historyResponseDTO.getAppUserId()) && 41 | history.getExercise().getName().equals(historyResponseDTO.getExerciseName()) && 42 | history.getWeight().equals(historyResponseDTO.getWeight()) && 43 | history.getReps().equals(historyResponseDTO.getReps()) && 44 | history.getCreatedOn().equals(historyResponseDTO.getCreatedOn())); 45 | } 46 | 47 | public static boolean isEqual(HistoryResponseDTO historyResponseDTO, History history) { 48 | return isEqual(history, historyResponseDTO); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /.github/workflows/pr-build.yml: -------------------------------------------------------------------------------- 1 | name: Build PR 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | 7 | jobs: 8 | build: 9 | name: Test PR 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 16 | 17 | - name: Set up JDK 11 18 | uses: actions/setup-java@v3 19 | with: 20 | java-version: '11' 21 | distribution: 'temurin' 22 | cache: maven 23 | 24 | - name: Cache local Maven repository 25 | uses: actions/cache@v3 26 | with: 27 | path: ~/.m2/repository 28 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 29 | restore-keys: | 30 | ${{ runner.os }}-maven- 31 | 32 | - name: Maven Test 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | run: | 36 | mvn -B test 37 | 38 | notify: 39 | needs: build 40 | name: Analyze PR 41 | runs-on: ubuntu-latest 42 | 43 | steps: 44 | - uses: actions/checkout@v3 45 | with: 46 | fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis 47 | 48 | - name: Set up JDK 11 49 | uses: actions/setup-java@v3 50 | with: 51 | java-version: '11' 52 | distribution: 'temurin' 53 | cache: maven 54 | 55 | - name: Cache SonarCloud packages 56 | uses: actions/cache@v3 57 | with: 58 | path: ~/.sonar/cache 59 | key: ${{ runner.os }}-sonar 60 | restore-keys: ${{ runner.os }}-sonar 61 | 62 | - name: Cache local Maven repository 63 | uses: actions/cache@v3 64 | with: 65 | path: ~/.m2/repository 66 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 67 | restore-keys: | 68 | ${{ runner.os }}-maven- 69 | 70 | - name: Analyze PR 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any 73 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 74 | run: | 75 | mvn -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar \ 76 | -Dsonar.host.url=https://sonarcloud.io \ 77 | -Dsonar.organization=fitbuddy \ 78 | -Dsonar.projectKey=fitbuddy-app 79 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 6 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 7 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 8 | import org.springframework.security.config.core.GrantedAuthorityDefaults; 9 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 10 | import org.springframework.security.web.SecurityFilterChain; 11 | 12 | /** 13 | * Security related configuration. 14 | */ 15 | @Configuration 16 | @EnableWebSecurity 17 | @EnableGlobalMethodSecurity(prePostEnabled = true) 18 | public class SecurityConfig { 19 | 20 | @Bean 21 | public BCryptPasswordEncoder passwordEncoder() { 22 | return new BCryptPasswordEncoder(); 23 | } 24 | 25 | @Bean 26 | public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { 27 | http.csrf().disable(); 28 | 29 | // User's page 30 | http.authorizeRequests().antMatchers("/user/**").hasAnyRole("USER", "ADMIN"); 31 | 32 | // H2 Console 33 | http.authorizeRequests().antMatchers("/console/**").permitAll(); 34 | http.headers().frameOptions().sameOrigin(); 35 | 36 | // URLs does not require login 37 | http.authorizeRequests().antMatchers("/login").permitAll(); 38 | http.authorizeRequests().antMatchers("/login/perform_login").permitAll(); 39 | http.authorizeRequests().antMatchers("/logout").permitAll(); 40 | http.authorizeRequests().antMatchers("/register").permitAll(); 41 | http.authorizeRequests().antMatchers("/version").permitAll(); 42 | http.authorizeRequests().antMatchers("/public/**").permitAll(); 43 | 44 | // Access denied for Admin's page 45 | http.authorizeRequests().and().exceptionHandling().accessDeniedPage("/403"); 46 | 47 | // Configure custom login form 48 | http.authorizeRequests().and().formLogin() 49 | .loginPage("/login"); // Sets the login page URL 50 | 51 | http.authorizeRequests().anyRequest().authenticated(); 52 | 53 | return http.build(); 54 | } 55 | 56 | @Bean 57 | GrantedAuthorityDefaults grantedAuthorityDefaults() { 58 | return new GrantedAuthorityDefaults(""); // Remove the ROLE_ prefix 59 | } 60 | 61 | } -------------------------------------------------------------------------------- /src/test/java/app/fitbuddy/service/operation/RegisterServiceTest.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.service.operation; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | import static org.mockito.ArgumentMatchers.any; 6 | import static org.mockito.ArgumentMatchers.anyString; 7 | import static org.mockito.Mockito.when; 8 | 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.api.extension.ExtendWith; 11 | import org.mockito.InjectMocks; 12 | import org.mockito.Mock; 13 | import org.mockito.junit.jupiter.MockitoExtension; 14 | 15 | import app.fitbuddy.dto.appuser.AppUserRequestDTO; 16 | import app.fitbuddy.dto.appuser.AppUserResponseDTO; 17 | import app.fitbuddy.dto.role.RoleResponseDTO; 18 | import app.fitbuddy.exception.FitBuddyException; 19 | import app.fitbuddy.service.crud.AppUserCrudService; 20 | import app.fitbuddy.service.crud.RoleCrudService; 21 | 22 | @ExtendWith(MockitoExtension.class) 23 | class RegisterServiceTest { 24 | 25 | @InjectMocks RegisterService instance; 26 | @Mock AppUserCrudService appUserCrudService; 27 | @Mock RoleCrudService roleCrudService; 28 | 29 | @Test 30 | void register_whenNameAlreadyExists_shouldThrowFitBuddyException() { 31 | AppUserResponseDTO appUserResponseDTO = new AppUserResponseDTO(1, "name", "password", "roleName"); 32 | 33 | when(appUserCrudService.readByName(anyString())).thenReturn(appUserResponseDTO); 34 | 35 | assertThrows(FitBuddyException.class, () -> instance.register("name", "password")); 36 | } 37 | 38 | @Test 39 | void register_whenUserRoleDoesntExists_shouldThrowFitBuddyException() { 40 | 41 | when(appUserCrudService.readByName(anyString())).thenReturn(null); 42 | when(roleCrudService.readByName(anyString())).thenReturn(null); 43 | 44 | assertThrows(FitBuddyException.class, () -> instance.register("name", "password")); 45 | } 46 | 47 | @Test 48 | void register_whenRegisterNewUser_shouldReturnNewUserId() { 49 | RoleResponseDTO roleResponseDTO = new RoleResponseDTO(1, "roleName"); 50 | AppUserResponseDTO appUserResponseDTO = new AppUserResponseDTO(123, "name", "encodedPassword", "roleName"); 51 | 52 | when(appUserCrudService.readByName(anyString())).thenReturn(null); 53 | when(roleCrudService.readByName(anyString())).thenReturn(roleResponseDTO); 54 | when(appUserCrudService.create(any(AppUserRequestDTO.class))).thenReturn(appUserResponseDTO); 55 | 56 | Integer newAppUserId = instance.register("name", "password"); 57 | 58 | assertEquals(123, newAppUserId); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/service/mapper/ExerciseMapperService.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.service.mapper; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | import java.util.Optional; 7 | 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Service; 10 | 11 | import app.fitbuddy.dto.exercise.ExerciseRequestDTO; 12 | import app.fitbuddy.dto.exercise.ExerciseResponseDTO; 13 | import app.fitbuddy.dto.exercise.ExerciseUpdateDTO; 14 | import app.fitbuddy.entity.AppUser; 15 | import app.fitbuddy.entity.Exercise; 16 | import app.fitbuddy.exception.FitBuddyException; 17 | import app.fitbuddy.repository.AppUserRepository; 18 | 19 | @Service 20 | public class ExerciseMapperService implements MapperService { 22 | 23 | private final AppUserRepository appUserRepository; 24 | 25 | @Autowired 26 | public ExerciseMapperService(AppUserRepository appUserRepository) { 27 | this.appUserRepository = appUserRepository; 28 | } 29 | 30 | @Override 31 | public Exercise requestDtoToEntity(ExerciseRequestDTO requestDTO) { 32 | if (requestDTO == null) { 33 | return null; 34 | } 35 | Optional optionalAppUser = appUserRepository.findById(requestDTO.getAppUserId()); 36 | if (optionalAppUser.isEmpty()) { 37 | throw new FitBuddyException("AppUser not found with ID: " + requestDTO.getAppUserId()); 38 | } 39 | Exercise exercise = new Exercise(); 40 | exercise.setName(requestDTO.getName()); 41 | exercise.setAppUser(optionalAppUser.get()); 42 | return exercise; 43 | } 44 | 45 | @Override 46 | public ExerciseResponseDTO entityToResponseDto(Exercise entity) { 47 | if (entity == null) { 48 | return null; 49 | } 50 | return new ExerciseResponseDTO(entity.getId(), entity.getName(), entity.getAppUser().getId()); 51 | } 52 | 53 | @Override 54 | public List entitiesToResponseDtos(List entities) { 55 | if (entities == null || entities.isEmpty()) { 56 | return Collections.emptyList(); 57 | } 58 | List result = new ArrayList<>(); 59 | for (Exercise entity : entities) { 60 | result.add(entityToResponseDto(entity)); 61 | } 62 | return result; 63 | } 64 | 65 | @Override 66 | public Exercise applyUpdateDtoToEntity(Exercise entity, ExerciseUpdateDTO updateDTO) { 67 | if (entity == null || updateDTO == null) { 68 | return null; 69 | } 70 | if (updateDTO.getName() != null) { 71 | entity.setName(updateDTO.getName()); 72 | } 73 | return entity; 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/main/resources/static/user/js/account.js: -------------------------------------------------------------------------------- 1 | const ACCOUNT_API_PATH = "/user/account"; 2 | 3 | function showUserName() { 4 | let xhr = new XMLHttpRequest(); 5 | xhr.open("GET", ACCOUNT_API_PATH); 6 | xhr.send(); 7 | 8 | xhr.onreadystatechange = function() { 9 | if (this.readyState == XMLHttpRequest.DONE) { 10 | if (this.status == 200) { 11 | // SUCCESS 12 | let data = JSON.parse(this.responseText); 13 | 14 | document.getElementById("logged-in-name").innerHTML += data.name; 15 | } else { 16 | // ERROR 17 | console.log("ERROR: " + this.responseText); 18 | } 19 | } 20 | }; 21 | } 22 | 23 | function showAccount() { 24 | let xhr = new XMLHttpRequest(); 25 | xhr.open("GET", ACCOUNT_API_PATH); 26 | xhr.send(); 27 | 28 | xhr.onreadystatechange = function() { 29 | if (this.readyState == XMLHttpRequest.DONE) { 30 | if (this.status == 200) { 31 | // SUCCESS 32 | let data = JSON.parse(this.responseText); 33 | 34 | clearFormValue("account-form", "old-password"); 35 | clearFormValue("account-form", "new-password"); 36 | clearFormValue("account-form", "confirm-new-password"); 37 | } else { 38 | // ERROR 39 | console.log("ERROR: " + this.responseText); 40 | } 41 | } 42 | }; 43 | } 44 | 45 | function updateAccount() { 46 | let oldPassword = document.forms["account-form"]["old-password"].value; 47 | let newPassword = document.forms["account-form"]["new-password"].value; 48 | let confirmNewPassword = document.forms["account-form"]["confirm-new-password"].value; 49 | 50 | oldPassword = oldPassword.trim(); 51 | newPassword = newPassword.trim(); 52 | confirmNewPassword = confirmNewPassword.trim(); 53 | 54 | if (newPassword !== confirmNewPassword) { 55 | let message = "New password and confirm new password doesn't match."; 56 | console.log("ERROR: " + message); 57 | showStatus(message); 58 | return; 59 | } 60 | 61 | let data = { "oldPassword" : oldPassword, 62 | "newPassword" : newPassword }; 63 | 64 | let xhr = new XMLHttpRequest(); 65 | xhr.open("PUT", ACCOUNT_API_PATH); 66 | xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); 67 | xhr.send(JSON.stringify(data)); 68 | 69 | xhr.onreadystatechange = function() { 70 | if (this.readyState == XMLHttpRequest.DONE) { 71 | if (this.status == 200) { 72 | // SUCCESS 73 | console.log("OK: " + this.responseText); 74 | } else { 75 | // ERROR 76 | console.log("ERROR: " + this.responseText); 77 | showStatus(this.responseText); 78 | } 79 | showAccount(); 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/controller/crud/AppUserController.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.controller.crud; 2 | 3 | import java.util.List; 4 | 5 | import javax.validation.Valid; 6 | import javax.validation.constraints.NotNull; 7 | 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.security.access.prepost.PreAuthorize; 12 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 13 | import org.springframework.web.bind.annotation.DeleteMapping; 14 | import org.springframework.web.bind.annotation.GetMapping; 15 | import org.springframework.web.bind.annotation.PathVariable; 16 | import org.springframework.web.bind.annotation.PostMapping; 17 | import org.springframework.web.bind.annotation.PutMapping; 18 | import org.springframework.web.bind.annotation.RequestBody; 19 | import org.springframework.web.bind.annotation.RequestMapping; 20 | import org.springframework.web.bind.annotation.RestController; 21 | 22 | import app.fitbuddy.dto.appuser.AppUserRequestDTO; 23 | import app.fitbuddy.dto.appuser.AppUserResponseDTO; 24 | import app.fitbuddy.dto.appuser.AppUserUpdateDTO; 25 | import app.fitbuddy.security.AppUserPrincipal; 26 | import app.fitbuddy.service.crud.AppUserCrudService; 27 | 28 | @RestController 29 | @RequestMapping("/users") 30 | @PreAuthorize("hasAuthority('ADMIN')") 31 | public class AppUserController { 32 | 33 | private final Logger logger; 34 | private final AppUserCrudService appUserCrudService; 35 | 36 | @Autowired 37 | public AppUserController(AppUserCrudService appUserCrudService) { 38 | this.appUserCrudService = appUserCrudService; 39 | this.logger = LoggerFactory.getLogger(AppUserController.class); 40 | } 41 | 42 | @PostMapping 43 | public void create(@Valid @RequestBody AppUserRequestDTO appUserRequestDTO) { 44 | appUserCrudService.create(appUserRequestDTO); 45 | logger.info("Creating new appUser: {}", appUserRequestDTO); 46 | } 47 | 48 | @GetMapping 49 | public List readAll() { 50 | return appUserCrudService.readAll(); 51 | } 52 | 53 | @PutMapping("{id}") 54 | public void update(@PathVariable("id") @NotNull Integer appUserId, 55 | @Valid @RequestBody AppUserUpdateDTO appUserUpdateDTO, 56 | @AuthenticationPrincipal AppUserPrincipal appUserPrincipal) { 57 | appUserCrudService.update(appUserPrincipal.getId(), appUserUpdateDTO); 58 | } 59 | 60 | @DeleteMapping("{id}") 61 | public void delete(@PathVariable("id") @NotNull Integer appUserId) { 62 | appUserCrudService.delete(appUserId); 63 | logger.info("Deleting AppUser with ID: {}" , appUserId); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/service/crud/HistoryCrudService.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.service.crud; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | import javax.validation.constraints.NotNull; 7 | 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Service; 10 | 11 | import app.fitbuddy.dto.history.HistoryRequestDTO; 12 | import app.fitbuddy.dto.history.HistoryResponseDTO; 13 | import app.fitbuddy.dto.history.HistoryUpdateDTO; 14 | import app.fitbuddy.entity.History; 15 | import app.fitbuddy.repository.HistoryRepository; 16 | import app.fitbuddy.service.mapper.HistoryMapperService; 17 | 18 | @Service 19 | public class HistoryCrudService implements CrudService { 20 | 21 | private final HistoryRepository historyRepository; 22 | private final HistoryMapperService historyMapperService; 23 | 24 | @Autowired 25 | public HistoryCrudService(HistoryRepository historyRepository, HistoryMapperService historyMapperService) { 26 | this.historyRepository = historyRepository; 27 | this.historyMapperService = historyMapperService; 28 | } 29 | 30 | @Override 31 | public HistoryResponseDTO create(HistoryRequestDTO requestDTO) { 32 | if (requestDTO == null) { 33 | return null; 34 | } 35 | History savedHistory = historyRepository.save(historyMapperService.requestDtoToEntity(requestDTO)); 36 | return historyMapperService.entityToResponseDto(savedHistory); 37 | } 38 | 39 | @Override 40 | public HistoryResponseDTO readById(Integer id) { 41 | Optional optionalHistory = historyRepository.findById(id); 42 | if (optionalHistory.isEmpty()) { 43 | return null; 44 | } 45 | return historyMapperService.entityToResponseDto(optionalHistory.get()); 46 | } 47 | 48 | @NotNull 49 | public List readMany(Integer appUserId, String date) { 50 | return historyMapperService.entitiesToResponseDtos(historyRepository.findAllByUserIdAndDate(appUserId, date)); 51 | } 52 | 53 | @Override 54 | public HistoryResponseDTO update(Integer id, HistoryUpdateDTO updateDTO) { 55 | if (updateDTO == null) { 56 | return null; 57 | } 58 | Optional optionalExistingHistory = historyRepository.findById(id); 59 | if (optionalExistingHistory.isEmpty()) { 60 | return null; 61 | } 62 | History savedHistory = historyRepository.save( 63 | historyMapperService.applyUpdateDtoToEntity(optionalExistingHistory.get(), updateDTO)); 64 | return historyMapperService.entityToResponseDto(savedHistory); 65 | } 66 | 67 | @Override 68 | public void delete(Integer id) { 69 | historyRepository.deleteById(id); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/test/java/app/fitbuddy/controller/operation/LoginControllerTest.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.controller.operation; 2 | 3 | import static org.mockito.Mockito.verify; 4 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 5 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 6 | 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.params.ParameterizedTest; 9 | import org.junit.jupiter.params.provider.ValueSource; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 12 | import org.springframework.boot.test.mock.mockito.MockBean; 13 | import org.springframework.http.MediaType; 14 | import org.springframework.test.context.ContextConfiguration; 15 | import org.springframework.test.web.servlet.MockMvc; 16 | 17 | import com.fasterxml.jackson.databind.ObjectMapper; 18 | 19 | import app.fitbuddy.FitBuddyApplication; 20 | import app.fitbuddy.config.SecurityConfig; 21 | import app.fitbuddy.dto.LoginDTO; 22 | import app.fitbuddy.service.operation.LoginService; 23 | 24 | @WebMvcTest(LoginController.class) 25 | @ContextConfiguration(classes = {FitBuddyApplication.class, SecurityConfig.class}) 26 | class LoginControllerTest { 27 | 28 | @Autowired MockMvc mockMvc; 29 | @Autowired ObjectMapper objectMapper; 30 | @MockBean LoginService loginService; 31 | 32 | final String API_PATH = "/login/perform_login"; 33 | 34 | @Test 35 | void login_whenInputIsCorrect_ShouldReturnOk() throws Exception { 36 | LoginDTO loginDtoMock = new LoginDTO("name", "password"); 37 | 38 | mockMvc.perform(post(API_PATH) 39 | .contentType(MediaType.APPLICATION_JSON) 40 | .content(objectMapper.writeValueAsString(loginDtoMock))) 41 | .andExpect(status().isOk()); 42 | 43 | verify(loginService).login(loginDtoMock); 44 | } 45 | 46 | @ParameterizedTest 47 | @ValueSource(strings = {"nam", "namenamenamename"}) // 3 and 16 characters 48 | void login_whenNameSizeNotCorrect_shouldReturnBadRequest(String name) throws Exception { 49 | LoginDTO loginDtoMock = new LoginDTO(name, "password"); 50 | 51 | mockMvc.perform(post(API_PATH) 52 | .contentType(MediaType.APPLICATION_JSON) 53 | .content(objectMapper.writeValueAsString(loginDtoMock))) 54 | .andExpect(status().isBadRequest()); 55 | } 56 | 57 | @ParameterizedTest 58 | @ValueSource(strings = {"pas", "passwordpassword"}) // 3 and 16 characters 59 | void login_whenPasswordSizeNotCorrect_shouldReturnBadRequest(String password) throws Exception { 60 | LoginDTO loginDtoMock = new LoginDTO("name", password); 61 | 62 | mockMvc.perform(post(API_PATH) 63 | .contentType(MediaType.APPLICATION_JSON) 64 | .content(objectMapper.writeValueAsString(loginDtoMock))) 65 | .andExpect(status().isBadRequest()); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/service/operation/LoginService.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.service.operation; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | import app.fitbuddy.dto.LoginDTO; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 11 | import org.springframework.security.core.Authentication; 12 | import org.springframework.security.core.GrantedAuthority; 13 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 14 | import org.springframework.security.core.context.SecurityContext; 15 | import org.springframework.security.core.context.SecurityContextHolder; 16 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 17 | import org.springframework.stereotype.Service; 18 | 19 | import app.fitbuddy.entity.AppUser; 20 | import app.fitbuddy.exception.FitBuddyException; 21 | import app.fitbuddy.repository.AppUserRepository; 22 | import app.fitbuddy.security.AppUserPrincipal; 23 | 24 | /** 25 | * Provides a service to handle the login process. 26 | */ 27 | @Service 28 | public class LoginService { 29 | 30 | private final Logger logger; 31 | private final AppUserRepository appUserRepository; 32 | private final BCryptPasswordEncoder bCryptPasswordEncoder; 33 | 34 | @Autowired 35 | public LoginService(AppUserRepository appUserRepository, BCryptPasswordEncoder bCryptPasswordEncoder) { 36 | this.appUserRepository = appUserRepository; 37 | this.bCryptPasswordEncoder = bCryptPasswordEncoder; 38 | this.logger = LoggerFactory.getLogger(LoginService.class); 39 | } 40 | 41 | public void login(LoginDTO loginDTO) { 42 | // find the user 43 | Optional optionalAppUser = appUserRepository.findByName(loginDTO.getName()); 44 | if (optionalAppUser.isEmpty()) { 45 | throw new FitBuddyException("Username not found."); 46 | } 47 | 48 | // check the password 49 | if (!bCryptPasswordEncoder.matches(loginDTO.getPassword(), optionalAppUser.get().getPassword())) { 50 | throw new FitBuddyException("Incorrect password."); 51 | } 52 | 53 | // create the authorities 54 | List authorities = List.of( 55 | new SimpleGrantedAuthority(optionalAppUser.get().getRole().getName())); 56 | 57 | // create a new authentication 58 | Authentication authentication = new UsernamePasswordAuthenticationToken( 59 | new AppUserPrincipal(optionalAppUser.get()), loginDTO.getPassword(), authorities); 60 | 61 | // create an empty SecurityContext and set the authentication 62 | SecurityContext context = SecurityContextHolder.createEmptyContext(); 63 | context.setAuthentication(authentication); 64 | 65 | // set the SecurityContext 66 | SecurityContextHolder.setContext(context); 67 | 68 | logger.info("Logged in: {}", optionalAppUser.get()); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/service/crud/RoleCrudService.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.service.crud; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | import javax.validation.constraints.NotNull; 7 | 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Service; 10 | 11 | import app.fitbuddy.dto.role.RoleRequestDTO; 12 | import app.fitbuddy.dto.role.RoleResponseDTO; 13 | import app.fitbuddy.dto.role.RoleUpdateDTO; 14 | import app.fitbuddy.entity.Role; 15 | import app.fitbuddy.exception.FitBuddyException; 16 | import app.fitbuddy.repository.RoleRepository; 17 | import app.fitbuddy.service.mapper.RoleMapperService; 18 | 19 | @Service 20 | public class RoleCrudService implements CrudService { 21 | 22 | private final RoleRepository roleRepository; 23 | private final RoleMapperService roleMapperService; 24 | 25 | @Autowired 26 | public RoleCrudService(RoleRepository roleRepository, RoleMapperService roleMapperService) { 27 | this.roleRepository = roleRepository; 28 | this.roleMapperService = roleMapperService; 29 | } 30 | 31 | @Override 32 | public RoleResponseDTO create(RoleRequestDTO requestDTO) { 33 | if (requestDTO == null) { 34 | return null; 35 | } 36 | if (roleRepository.findByName(requestDTO.getName()).isPresent()) { 37 | throw new FitBuddyException("Role name already exists."); 38 | } 39 | Role savedRole = roleRepository.save(roleMapperService.requestDtoToEntity(requestDTO)); 40 | return roleMapperService.entityToResponseDto(savedRole); 41 | } 42 | 43 | @Override 44 | public RoleResponseDTO readById(Integer id) { 45 | Optional optionalRole = roleRepository.findById(id); 46 | if (optionalRole.isEmpty()) { 47 | return null; 48 | } 49 | return roleMapperService.entityToResponseDto(optionalRole.get()); 50 | } 51 | 52 | public RoleResponseDTO readByName(String name) { 53 | Optional optionalRole = roleRepository.findByName(name); 54 | if (optionalRole.isEmpty()) { 55 | return null; 56 | } 57 | return roleMapperService.entityToResponseDto(optionalRole.get()); 58 | } 59 | 60 | @NotNull 61 | public List readAll() { 62 | return roleMapperService.entitiesToResponseDtos((List) roleRepository.findAll()); 63 | } 64 | 65 | @Override 66 | public RoleResponseDTO update(Integer id, RoleUpdateDTO updateDTO) { 67 | if (updateDTO == null) { 68 | return null; 69 | } 70 | Optional optionalExistingRole = roleRepository.findById(id); 71 | if (optionalExistingRole.isEmpty()) { 72 | return null; 73 | } 74 | if (!optionalExistingRole.get().getName().equals(updateDTO.getName()) && 75 | roleRepository.findByName(updateDTO.getName()).isPresent()) { 76 | throw new FitBuddyException("Role name already exists."); 77 | } 78 | Role savedRole = roleRepository.save( 79 | roleMapperService.applyUpdateDtoToEntity(optionalExistingRole.get(), updateDTO)); 80 | return roleMapperService.entityToResponseDto(savedRole); 81 | } 82 | 83 | @Override 84 | public void delete(Integer id) { 85 | roleRepository.deleteById(id); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /src/test/java/app/fitbuddy/controller/operation/RegisterControllerTest.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.controller.operation; 2 | 3 | import static org.mockito.ArgumentMatchers.anyInt; 4 | import static org.mockito.ArgumentMatchers.anyString; 5 | import static org.mockito.Mockito.verify; 6 | import static org.mockito.Mockito.when; 7 | 8 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 9 | 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.params.ParameterizedTest; 12 | import org.junit.jupiter.params.provider.ValueSource; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 15 | import org.springframework.boot.test.mock.mockito.MockBean; 16 | import org.springframework.http.MediaType; 17 | import org.springframework.test.context.ContextConfiguration; 18 | import org.springframework.test.web.servlet.MockMvc; 19 | import org.springframework.test.web.servlet.result.MockMvcResultMatchers; 20 | 21 | import com.fasterxml.jackson.databind.ObjectMapper; 22 | 23 | import app.fitbuddy.FitBuddyApplication; 24 | import app.fitbuddy.config.SecurityConfig; 25 | import app.fitbuddy.dto.RegisterDTO; 26 | import app.fitbuddy.service.operation.NewUserService; 27 | import app.fitbuddy.service.operation.RegisterService; 28 | 29 | @WebMvcTest(RegisterController.class) 30 | @ContextConfiguration(classes = {FitBuddyApplication.class, SecurityConfig.class}) 31 | class RegisterControllerTest { 32 | 33 | @Autowired MockMvc mockMvc; 34 | @Autowired ObjectMapper objectMapper; 35 | @MockBean RegisterService registerService; 36 | @MockBean NewUserService newUserService; 37 | 38 | @Test 39 | void register_whenInputIsCorrect_shouldReturnOk() throws Exception { 40 | RegisterDTO registerDtoMock = new RegisterDTO("name", "password"); 41 | 42 | when(registerService.register(anyString(), anyString())).thenReturn(1); 43 | 44 | mockMvc.perform(post("/register") 45 | .contentType(MediaType.APPLICATION_JSON) 46 | .content(objectMapper.writeValueAsString(registerDtoMock))) 47 | .andExpect(MockMvcResultMatchers.status().isOk()); 48 | 49 | verify(registerService).register(anyString(), anyString()); 50 | verify(newUserService).addDefaultExercises(anyInt()); 51 | } 52 | 53 | @ParameterizedTest 54 | @ValueSource(strings = {"nam", "namenamenamename"}) // 3 and 16 characters 55 | void register_whenNameSizeNotCorrect_shouldReturnBadRequest(String name) throws Exception { 56 | RegisterDTO registerDtoMock = new RegisterDTO(name, "password"); 57 | 58 | mockMvc.perform(post("/register") 59 | .contentType(MediaType.APPLICATION_JSON) 60 | .content(objectMapper.writeValueAsString(registerDtoMock))) 61 | .andExpect(MockMvcResultMatchers.status().isBadRequest()); 62 | } 63 | 64 | @ParameterizedTest 65 | @ValueSource(strings = {"pas", "passwordpassword"}) // 3 and 16 characters 66 | void register_whenPasswordSizeNotCorrect_shouldReturnBadRequest(String password) throws Exception { 67 | RegisterDTO registerDtoMock = new RegisterDTO("name", password); 68 | 69 | mockMvc.perform(post("/register") 70 | .contentType(MediaType.APPLICATION_JSON) 71 | .content(objectMapper.writeValueAsString(registerDtoMock))) 72 | .andExpect(MockMvcResultMatchers.status().isBadRequest()); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/controller/crud/ExerciseController.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.controller.crud; 2 | 3 | import app.fitbuddy.dto.exercise.ExerciseRequestDTO; 4 | import app.fitbuddy.dto.exercise.ExerciseResponseDTO; 5 | import app.fitbuddy.dto.exercise.ExerciseUpdateDTO; 6 | import app.fitbuddy.exception.FitBuddyException; 7 | import app.fitbuddy.security.AppUserPrincipal; 8 | import app.fitbuddy.service.crud.ExerciseCrudService; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.security.access.prepost.PreAuthorize; 13 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 14 | import org.springframework.web.bind.annotation.*; 15 | 16 | import javax.validation.Valid; 17 | import javax.validation.constraints.NotNull; 18 | import java.util.List; 19 | 20 | @RestController 21 | @RequestMapping("/user/exercises") 22 | @PreAuthorize("authenticated") 23 | public class ExerciseController { 24 | 25 | private final Logger logger; 26 | private final ExerciseCrudService exerciseCrudService; 27 | 28 | @Autowired 29 | public ExerciseController(ExerciseCrudService exerciseCrudService) { 30 | this.exerciseCrudService = exerciseCrudService; 31 | this.logger = LoggerFactory.getLogger(ExerciseController.class); 32 | } 33 | 34 | @PostMapping 35 | public void create(@RequestBody @Valid ExerciseRequestDTO exerciseRequestDTO, 36 | @AuthenticationPrincipal AppUserPrincipal appUserPrincipal) { 37 | exerciseRequestDTO.setAppUserId(appUserPrincipal.getId()); 38 | exerciseCrudService.create(exerciseRequestDTO); 39 | logger.info("Creating new exercise: {}", exerciseRequestDTO); 40 | } 41 | 42 | @GetMapping 43 | public List readAll(@AuthenticationPrincipal AppUserPrincipal appUserPrincipal) { 44 | List responseDTOs = exerciseCrudService.readMany(appUserPrincipal.getId()); 45 | logger.info("Sending a list of exercises: {}", responseDTOs); 46 | return responseDTOs; 47 | } 48 | 49 | @PutMapping("{id}") 50 | public void update(@PathVariable("id") @NotNull Integer exerciseId, 51 | @Valid @RequestBody ExerciseUpdateDTO exerciseUpdateDTO, 52 | @AuthenticationPrincipal AppUserPrincipal appUserPrincipal) { 53 | ExerciseResponseDTO exerciseResponseDTO = exerciseCrudService.readById(exerciseId); 54 | if (exerciseResponseDTO != null && exerciseResponseDTO.getAppUserId().equals(appUserPrincipal.getId())) { 55 | exerciseCrudService.update(exerciseId, exerciseUpdateDTO); 56 | logger.info("Updating the exercise: {}", exerciseUpdateDTO); 57 | } else { 58 | throw new FitBuddyException("UserIds doesn't match. Cannot update others Exercise."); 59 | } 60 | } 61 | 62 | @DeleteMapping("{id}") 63 | public void delete(@PathVariable("id") @NotNull Integer exerciseId, 64 | @AuthenticationPrincipal AppUserPrincipal appUserPrincipal) { 65 | ExerciseResponseDTO exerciseResponseDTO = exerciseCrudService.readById(exerciseId); 66 | if (exerciseResponseDTO != null && exerciseResponseDTO.getAppUserId().equals(appUserPrincipal.getId())) { 67 | exerciseCrudService.delete(exerciseId); 68 | logger.info("Deleting exercise: {}", exerciseResponseDTO); 69 | } else { 70 | throw new FitBuddyException("UserIds doesn't match. Cannot delete others Exercise."); 71 | } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/service/crud/AppUserCrudService.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.service.crud; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | import javax.validation.constraints.NotNull; 7 | 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Service; 10 | 11 | import app.fitbuddy.dto.appuser.AppUserRequestDTO; 12 | import app.fitbuddy.dto.appuser.AppUserResponseDTO; 13 | import app.fitbuddy.dto.appuser.AppUserUpdateDTO; 14 | import app.fitbuddy.entity.AppUser; 15 | import app.fitbuddy.exception.FitBuddyException; 16 | import app.fitbuddy.repository.AppUserRepository; 17 | import app.fitbuddy.service.mapper.AppUserMapperService; 18 | 19 | @Service 20 | public class AppUserCrudService implements CrudService { 21 | 22 | private final AppUserRepository appUserRepository; 23 | private final AppUserMapperService appUserMapperService; 24 | 25 | @Autowired 26 | public AppUserCrudService(AppUserRepository appUserRepository, AppUserMapperService appUserMapperService) { 27 | this.appUserRepository = appUserRepository; 28 | this.appUserMapperService = appUserMapperService; 29 | } 30 | 31 | @Override 32 | public AppUserResponseDTO create(AppUserRequestDTO requestDTO) { 33 | if (requestDTO == null) { 34 | return null; 35 | } 36 | if (appUserRepository.findByName(requestDTO.getName()).isPresent()) { 37 | throw new FitBuddyException("Username already exists."); 38 | } 39 | AppUser savedAppUser = appUserRepository.save(appUserMapperService.requestDtoToEntity(requestDTO)); 40 | return appUserMapperService.entityToResponseDto(savedAppUser); 41 | } 42 | 43 | @Override 44 | public AppUserResponseDTO readById(Integer id) { 45 | Optional optionalAppUser = appUserRepository.findById(id); 46 | if (optionalAppUser.isEmpty()) { 47 | return null; 48 | } 49 | return appUserMapperService.entityToResponseDto(optionalAppUser.get()); 50 | } 51 | 52 | public AppUserResponseDTO readByName(String name) { 53 | Optional optionalAppUser = appUserRepository.findByName(name); 54 | if (optionalAppUser.isEmpty()) { 55 | return null; 56 | } 57 | return appUserMapperService.entityToResponseDto(optionalAppUser.get()); 58 | } 59 | 60 | @NotNull 61 | public List readAll() { 62 | return appUserMapperService.entitiesToResponseDtos((List) appUserRepository.findAll()); 63 | } 64 | 65 | @Override 66 | public AppUserResponseDTO update(Integer id, AppUserUpdateDTO updateDTO ) { 67 | if (updateDTO == null) { 68 | return null; 69 | } 70 | Optional optionalExistingAppUser = appUserRepository.findById(id); 71 | if (optionalExistingAppUser.isEmpty()) { 72 | return null; 73 | } 74 | if (!optionalExistingAppUser.get().getName().equals(updateDTO.getName()) && 75 | appUserRepository.findByName(updateDTO.getName()).isPresent()) { 76 | throw new FitBuddyException("Username already exists."); 77 | } 78 | AppUser savedAppUser = appUserRepository.save( 79 | appUserMapperService.applyUpdateDtoToEntity(optionalExistingAppUser.get(), updateDTO)); 80 | return appUserMapperService.entityToResponseDto(savedAppUser); 81 | } 82 | 83 | @Override 84 | public void delete(Integer id) { 85 | appUserRepository.deleteById(id); 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 2.7.4 10 | 11 | 12 | 13 | app.fitbuddy 14 | fitbuddy 15 | 0.0.1-SNAPSHOT 16 | FitBuddy 17 | FitBuddy, a workout tracker app 18 | 19 | 20 | 11 21 | 22 | 23 | 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-data-jpa 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-security 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-validation 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-web 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-starter-test 44 | test 45 | 46 | 47 | org.springframework.security 48 | spring-security-test 49 | test 50 | 51 | 52 | 53 | 54 | com.h2database 55 | h2 56 | 2.1.212 57 | runtime 58 | 59 | 60 | 61 | 62 | org.projectlombok 63 | lombok 64 | 1.18.24 65 | provided 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | org.springframework.boot 74 | spring-boot-maven-plugin 75 | 76 | 77 | 78 | 79 | org.jacoco 80 | jacoco-maven-plugin 81 | 0.8.8 82 | 83 | 84 | prepare-agent 85 | 86 | prepare-agent 87 | 88 | 89 | 90 | report 91 | 92 | report 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/service/crud/ExerciseCrudService.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.service.crud; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | import javax.validation.constraints.NotNull; 7 | 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.dao.DataIntegrityViolationException; 10 | import org.springframework.stereotype.Service; 11 | 12 | import app.fitbuddy.dto.exercise.ExerciseRequestDTO; 13 | import app.fitbuddy.dto.exercise.ExerciseResponseDTO; 14 | import app.fitbuddy.dto.exercise.ExerciseUpdateDTO; 15 | import app.fitbuddy.entity.Exercise; 16 | import app.fitbuddy.exception.FitBuddyException; 17 | import app.fitbuddy.repository.ExerciseRepository; 18 | import app.fitbuddy.service.mapper.ExerciseMapperService; 19 | 20 | @Service 21 | public class ExerciseCrudService implements CrudService { 22 | 23 | private final ExerciseRepository exerciseRepository; 24 | private final ExerciseMapperService exerciseMapperService; 25 | 26 | @Autowired 27 | public ExerciseCrudService(ExerciseRepository exerciseRepository, 28 | ExerciseMapperService exerciseMapperService) { 29 | this.exerciseRepository = exerciseRepository; 30 | this.exerciseMapperService = exerciseMapperService; 31 | } 32 | 33 | @Override 34 | public ExerciseResponseDTO create(ExerciseRequestDTO requestDTO) { 35 | if (requestDTO == null) { 36 | return null; 37 | } 38 | if (exerciseRepository.findByNameAndUserId(requestDTO.getName(), requestDTO.getAppUserId()).isPresent()) { 39 | throw new FitBuddyException("Exercise name already exists."); 40 | 41 | } 42 | Exercise savedExercise = exerciseRepository.save(exerciseMapperService.requestDtoToEntity(requestDTO)); 43 | return exerciseMapperService.entityToResponseDto(savedExercise); 44 | } 45 | 46 | @Override 47 | public ExerciseResponseDTO readById(Integer id) { 48 | Optional optionalExercise = exerciseRepository.findById(id); 49 | if (optionalExercise.isEmpty()) { 50 | return null; 51 | } 52 | return exerciseMapperService.entityToResponseDto(optionalExercise.get()); 53 | } 54 | 55 | @NotNull 56 | public List readMany(Integer appUserId) { 57 | return exerciseMapperService.entitiesToResponseDtos(exerciseRepository.findAllByUserId(appUserId)); 58 | } 59 | 60 | @Override 61 | public ExerciseResponseDTO update(Integer id, ExerciseUpdateDTO updateDTO) { 62 | if (updateDTO == null) { 63 | return null; 64 | } 65 | Optional optionalExistingExercise = exerciseRepository.findById(id); 66 | if (optionalExistingExercise.isEmpty()) { 67 | return null; 68 | } 69 | if (!optionalExistingExercise.get().getName().equals(updateDTO.getName()) && 70 | exerciseRepository.findByName(updateDTO.getName()).isPresent()) { 71 | throw new FitBuddyException("Exercise name already exists."); 72 | } 73 | Exercise savedExercise = exerciseRepository.save( 74 | exerciseMapperService.applyUpdateDtoToEntity(optionalExistingExercise.get(), updateDTO)); 75 | return exerciseMapperService.entityToResponseDto(savedExercise); 76 | } 77 | 78 | @Override 79 | public void delete(Integer id) { 80 | try { 81 | exerciseRepository.deleteById(id); 82 | } catch (DataIntegrityViolationException e) { 83 | throw new FitBuddyException("Exercise cannot be deleted, it's in use."); 84 | } 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/service/mapper/AppUserMapperService.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.service.mapper; 2 | 3 | import app.fitbuddy.dto.appuser.AppUserRequestDTO; 4 | import app.fitbuddy.dto.appuser.AppUserResponseDTO; 5 | import app.fitbuddy.dto.appuser.AppUserUpdateDTO; 6 | import app.fitbuddy.entity.AppUser; 7 | import app.fitbuddy.entity.Role; 8 | import app.fitbuddy.exception.FitBuddyException; 9 | import app.fitbuddy.repository.RoleRepository; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 12 | import org.springframework.stereotype.Service; 13 | 14 | import java.util.ArrayList; 15 | import java.util.Collections; 16 | import java.util.List; 17 | import java.util.Optional; 18 | 19 | @Service 20 | public class AppUserMapperService implements MapperService { 22 | 23 | private final RoleRepository roleRepository; 24 | private final BCryptPasswordEncoder bCryptPasswordEncoder; 25 | 26 | @Autowired 27 | public AppUserMapperService(RoleRepository roleRepository, BCryptPasswordEncoder bCryptPasswordEncoder) { 28 | this.roleRepository = roleRepository; 29 | this.bCryptPasswordEncoder = bCryptPasswordEncoder; 30 | } 31 | 32 | @Override 33 | public AppUser requestDtoToEntity(AppUserRequestDTO requestDTO) { 34 | if (requestDTO == null) { 35 | return null; 36 | } 37 | Optional optionalRole = roleRepository.findByName(requestDTO.getRolename()); 38 | if (optionalRole.isEmpty()) { 39 | throw new FitBuddyException("Role doesn't exists with name: " + 40 | requestDTO.getRolename()); 41 | } 42 | AppUser appUser = new AppUser(); 43 | appUser.setName(requestDTO.getName()); 44 | appUser.setPassword(bCryptPasswordEncoder.encode(requestDTO.getPassword())); 45 | appUser.setRole(optionalRole.get()); 46 | return appUser; 47 | } 48 | 49 | @Override 50 | public AppUserResponseDTO entityToResponseDto(AppUser entity) { 51 | if (entity == null) { 52 | return null; 53 | } 54 | return new AppUserResponseDTO(entity.getId(), entity.getName(), entity.getPassword(), 55 | entity.getRole().getName()); 56 | } 57 | 58 | @Override 59 | public List entitiesToResponseDtos(List entities) { 60 | if (entities == null || entities.isEmpty()) { 61 | return Collections.emptyList(); 62 | } 63 | List result = new ArrayList<>(); 64 | for (AppUser entity : entities) { 65 | result.add(entityToResponseDto(entity)); 66 | } 67 | return result; 68 | } 69 | 70 | @Override 71 | public AppUser applyUpdateDtoToEntity(AppUser entity, AppUserUpdateDTO updateDTO) { 72 | if (entity == null || updateDTO == null) { 73 | return null; 74 | } 75 | if (updateDTO.getName() != null) { 76 | entity.setName(updateDTO.getName()); 77 | } 78 | if (updateDTO.getPassword() != null) { 79 | entity.setPassword(bCryptPasswordEncoder.encode(updateDTO.getPassword())); 80 | } 81 | if (updateDTO.getRolename() != null) { 82 | Optional optionalRole = roleRepository.findByName(updateDTO.getRolename()); 83 | if (optionalRole.isEmpty()) { 84 | throw new FitBuddyException("Role doesn't exists with name: " + 85 | updateDTO.getRolename()); 86 | } 87 | entity.setRole(optionalRole.get()); 88 | } 89 | return entity; 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/service/mapper/HistoryMapperService.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.service.mapper; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | import java.util.Optional; 7 | 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Service; 10 | 11 | import app.fitbuddy.dto.history.HistoryRequestDTO; 12 | import app.fitbuddy.dto.history.HistoryResponseDTO; 13 | import app.fitbuddy.dto.history.HistoryUpdateDTO; 14 | import app.fitbuddy.entity.AppUser; 15 | import app.fitbuddy.entity.Exercise; 16 | import app.fitbuddy.entity.History; 17 | import app.fitbuddy.exception.FitBuddyException; 18 | import app.fitbuddy.repository.AppUserRepository; 19 | import app.fitbuddy.repository.ExerciseRepository; 20 | 21 | @Service 22 | public class HistoryMapperService implements MapperService { 24 | 25 | private final AppUserRepository appUserRepository; 26 | private final ExerciseRepository exerciseRepository; 27 | 28 | @Autowired 29 | public HistoryMapperService(AppUserRepository appUserRepository, 30 | ExerciseRepository exerciseRepository) { 31 | this.appUserRepository = appUserRepository; 32 | this.exerciseRepository = exerciseRepository; 33 | } 34 | 35 | @Override 36 | public History requestDtoToEntity(HistoryRequestDTO requestDTO) { 37 | if (requestDTO == null) { 38 | return null; 39 | } 40 | Optional optionalAppUser = appUserRepository.findById(requestDTO.getAppUserId()); 41 | if (optionalAppUser.isEmpty()) { 42 | throw new FitBuddyException("AppUser not found with ID: " + requestDTO.getAppUserId()); 43 | } 44 | Optional optionalExercise = exerciseRepository.findByNameAndUserId(requestDTO.getExerciseName(), 45 | requestDTO.getAppUserId()); 46 | if (optionalExercise.isEmpty()) { 47 | throw new FitBuddyException("Exercise not found with name and ID: " + requestDTO.getExerciseName() + 48 | ", " + requestDTO.getAppUserId()); 49 | } 50 | History history = new History(); 51 | history.setAppUser(optionalAppUser.get()); 52 | history.setExercise(optionalExercise.get()); 53 | history.setWeight(requestDTO.getWeight()); 54 | history.setReps(requestDTO.getReps()); 55 | history.setCreatedOn(requestDTO.getCreatedOn()); 56 | return history; 57 | } 58 | 59 | @Override 60 | public HistoryResponseDTO entityToResponseDto(History entity) { 61 | if (entity == null) { 62 | return null; 63 | } 64 | return new HistoryResponseDTO(entity.getId(), entity.getAppUser().getId(), entity.getExercise().getName(), 65 | entity.getWeight(), entity.getReps(), entity.getCreatedOn()); 66 | } 67 | 68 | @Override 69 | public List entitiesToResponseDtos(List entities) { 70 | if (entities == null || entities.isEmpty()) { 71 | return Collections.emptyList(); 72 | } 73 | List result = new ArrayList<>(); 74 | for (History entity : entities) { 75 | result.add(entityToResponseDto(entity)); 76 | } 77 | return result; 78 | } 79 | 80 | @Override 81 | public History applyUpdateDtoToEntity(History entity, HistoryUpdateDTO updateDTO) { 82 | if (entity == null || updateDTO == null) { 83 | return null; 84 | } 85 | if (updateDTO.getWeight() != null) { 86 | entity.setWeight(updateDTO.getWeight()); 87 | } 88 | if (updateDTO.getReps() != null) { 89 | entity.setReps(updateDTO.getReps()); 90 | } 91 | return entity; 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/test/java/app/fitbuddy/service/crud/HistoryCrudServiceTest.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.service.crud; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | import static org.mockito.ArgumentMatchers.any; 5 | import static org.mockito.ArgumentMatchers.anyInt; 6 | import static org.mockito.Mockito.verify; 7 | import static org.mockito.Mockito.when; 8 | 9 | import java.util.Optional; 10 | 11 | import org.junit.jupiter.api.Nested; 12 | import org.junit.jupiter.api.Test; 13 | import org.junit.jupiter.api.extension.ExtendWith; 14 | import org.mockito.InjectMocks; 15 | import org.mockito.Mock; 16 | import org.mockito.junit.jupiter.MockitoExtension; 17 | 18 | import app.fitbuddy.dto.history.HistoryRequestDTO; 19 | import app.fitbuddy.dto.history.HistoryUpdateDTO; 20 | import app.fitbuddy.entity.History; 21 | import app.fitbuddy.repository.HistoryRepository; 22 | import app.fitbuddy.service.mapper.HistoryMapperService; 23 | import app.fitbuddy.testhelper.HistoryTestHelper; 24 | 25 | @ExtendWith(MockitoExtension.class) 26 | class HistoryCrudServiceTest { 27 | 28 | @InjectMocks 29 | HistoryCrudService historyCrudService; 30 | 31 | @Mock 32 | HistoryRepository historyRepository; 33 | 34 | @Mock 35 | HistoryMapperService historyMapperService; 36 | 37 | @Nested 38 | class Create { 39 | 40 | @Test 41 | void requestDTOIsNull_returnNull() { 42 | assertNull(historyCrudService.create(null)); 43 | } 44 | 45 | @Test 46 | void callSave() { 47 | HistoryRequestDTO requestDTO = new HistoryRequestDTO(11, "exerciseName", 22, 33, "2022-01-01"); 48 | History history = HistoryTestHelper.getMockHistory(); 49 | 50 | when(historyMapperService.requestDtoToEntity(requestDTO)).thenReturn(history); 51 | 52 | historyCrudService.create(requestDTO); 53 | 54 | verify(historyRepository).save(history); 55 | } 56 | } 57 | 58 | @Nested 59 | class ReadById { 60 | 61 | @Test 62 | void notFoundById_returnNull() { 63 | when(historyRepository.findById(anyInt())).thenReturn(Optional.empty()); 64 | 65 | assertNull(historyCrudService.readById(1)); 66 | } 67 | } 68 | 69 | @Nested 70 | class ReadMany { 71 | 72 | @Test 73 | void callHistoryRepository() { 74 | historyCrudService.readMany(1, "2022-01-01"); 75 | 76 | verify(historyRepository).findAllByUserIdAndDate(1, "2022-01-01"); 77 | } 78 | } 79 | 80 | @Nested 81 | class Update { 82 | 83 | @Test 84 | void updateDTOIsNull_returnNull() { 85 | assertNull(historyCrudService.update(1, null)); 86 | } 87 | 88 | @Test 89 | void existingHistoryNotFound_returnNull() { 90 | HistoryUpdateDTO updateDTO = new HistoryUpdateDTO(11, 22); 91 | 92 | when(historyRepository.findById(anyInt())).thenReturn(Optional.empty()); 93 | 94 | assertNull(historyCrudService.update(1, updateDTO)); 95 | } 96 | 97 | @Test 98 | void applyAndSave() { 99 | HistoryUpdateDTO updateDTO = new HistoryUpdateDTO(11, 22); 100 | History history = HistoryTestHelper.getMockHistory(); 101 | 102 | when(historyRepository.findById(anyInt())).thenReturn(Optional.of(history)); 103 | when(historyMapperService.applyUpdateDtoToEntity(any(History.class), 104 | any(HistoryUpdateDTO.class))).thenReturn(history); 105 | 106 | historyCrudService.update(1, updateDTO); 107 | 108 | verify(historyMapperService).applyUpdateDtoToEntity(history, updateDTO); 109 | verify(historyRepository).save(history); 110 | } 111 | } 112 | 113 | @Nested 114 | class Delete { 115 | 116 | @Test 117 | void callDeleteById() { 118 | historyCrudService.delete(111); 119 | 120 | verify(historyRepository).deleteById(111); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/test/java/app/fitbuddy/service/operation/NewUserServiceTest.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.service.operation; 2 | 3 | import app.fitbuddy.dto.exercise.ExerciseRequestDTO; 4 | import app.fitbuddy.entity.DefaultExercise; 5 | import app.fitbuddy.exception.FitBuddyException; 6 | import app.fitbuddy.repository.DefaultExerciseRepository; 7 | import app.fitbuddy.service.crud.ExerciseCrudService; 8 | import app.fitbuddy.testhelper.DefaultExerciseTestHelper; 9 | 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | import org.junit.jupiter.params.ParameterizedTest; 13 | import org.junit.jupiter.params.provider.NullSource; 14 | import org.junit.jupiter.params.provider.ValueSource; 15 | 16 | import org.mockito.ArgumentCaptor; 17 | import org.mockito.InjectMocks; 18 | import org.mockito.Mock; 19 | import org.mockito.junit.jupiter.MockitoExtension; 20 | 21 | import java.util.List; 22 | 23 | import static org.junit.jupiter.api.Assertions.assertThrows; 24 | import static org.junit.jupiter.api.Assertions.assertTrue; 25 | import static org.mockito.Mockito.*; 26 | 27 | @ExtendWith(MockitoExtension.class) 28 | class NewUserServiceTest { 29 | private final static int DUMMY_USER_ID = 17; 30 | @InjectMocks NewUserService newUserService; 31 | @Mock DefaultExerciseRepository defaultExerciseRepository; 32 | @Mock ExerciseCrudService exerciseCrudService; 33 | 34 | @Test 35 | void newUser_whenAddsDefaultExercises_shouldFindAllDefaultExercises() { 36 | when(defaultExerciseRepository.findAll()).thenReturn(dummyDefaultExercises()); 37 | 38 | newUserService.addDefaultExercises(DUMMY_USER_ID); 39 | 40 | verify(defaultExerciseRepository).findAll(); 41 | } 42 | 43 | @Test 44 | void newUser_whenAddsDefaultExercises_shouldCreateDtoForEachDefaultExercise() { 45 | when(defaultExerciseRepository.findAll()).thenReturn(dummyDefaultExercises()); 46 | 47 | newUserService.addDefaultExercises(DUMMY_USER_ID); 48 | 49 | verify(exerciseCrudService, times(dummyDefaultExercises().size())).create(any()); 50 | } 51 | 52 | @Test 53 | void newUser_whenAddsDefaultExercises_shouldCreateCorrespondingExerciseDtoFromDefaultExercises() { 54 | when(defaultExerciseRepository.findAll()).thenReturn(dummyDefaultExercises()); 55 | ArgumentCaptor requestDTOCaptor = ArgumentCaptor.forClass(ExerciseRequestDTO.class); 56 | 57 | newUserService.addDefaultExercises(DUMMY_USER_ID); 58 | 59 | verify(exerciseCrudService, times(dummyDefaultExercises().size())).create(requestDTOCaptor.capture()); 60 | List capturedRequestDTOs = requestDTOCaptor.getAllValues(); 61 | dummyDefaultExercises().forEach( 62 | dummyDefaultExercise -> { 63 | assertTrue(hasCorrespondingDto(capturedRequestDTOs, dummyDefaultExercise)); 64 | } 65 | ); 66 | } 67 | 68 | @ParameterizedTest 69 | @NullSource 70 | @ValueSource(ints = {-7, -1, -1000001}) 71 | void newUser_whenAppUserIsNullOrNegative_shouldThrowException(Integer appUserId) { 72 | assertThrows(FitBuddyException.class, () -> { 73 | newUserService.addDefaultExercises(appUserId); 74 | }); 75 | } 76 | 77 | /** 78 | * Checks that captured ExerciseRequestDTO list contains an exercise with expected name and appUserId DUMMY_USER_ID 79 | */ 80 | private boolean hasCorrespondingDto(List capturedRequestDTOs, 81 | DefaultExercise defaultExercise) { 82 | return capturedRequestDTOs.stream() 83 | .anyMatch(dto -> DefaultExerciseTestHelper.isEqual(dto, defaultExercise) && 84 | dto.getAppUserId().equals(DUMMY_USER_ID)); 85 | } 86 | 87 | private List dummyDefaultExercises() { 88 | return List.of( 89 | DefaultExerciseTestHelper.getMockDefaultExercise(17, "walk out and squats"), 90 | DefaultExerciseTestHelper.getMockDefaultExercise(88, "plank"), 91 | DefaultExerciseTestHelper.getMockDefaultExercise(7, "push ups") 92 | ); 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guidelines 2 | 3 | Contributions are welcome. 👍 4 | 5 | Please follow the guide described below how can you contribute to this project. 6 | 7 | ## Submitting Issues 8 | 9 | - Before submitting any new issues, please make sure there are no similar issues has been opened already. [See all issues in the project](https://github.com/mepox/fitbuddy/issues) 10 | - Please use the GitHub web interface to create and submit a new issue. [Submit a new issue here](https://github.com/mepox/fitbuddy/issues/new) 11 | - The following issue types are welcome (for now): 12 | - 🐛 bugs: Have you found bugs in the project that no one noticed yet? 13 | - ✍️ typos: Have you noticed a typo in the project? 14 | - 📰 docs: Documents are never enough and can be always improved. 15 | - 💼 code: The code is still WIP, but if you have found something that could be done better then feel free to report it. Feedbacks are always welcome! 16 | - 🔆 ideas: Ideas are welcome, just please note the project still in WIP. 17 | - ❓ question: Have a question? Feel free to open a new issue. 18 | 19 | ## Working on Issues 20 | 21 | - Issues open for contributions are usually labelled with `help wanted`. [See all help wanted issues in the project](https://github.com/mepox/fitbuddy/labels/help%20wanted) 22 | - Before you start working on an issue, please make sure you did one of the following: 23 | - Ask to get assigned first. When someone got assigned to an issue then it's very clear for everyone who is working on it! 24 | - Ask or at least let everyone know in the comments that you are working on the issue. 25 | 26 | ❗ *By not following these simple rules could lead to confusion and disappointment!* ❗ 27 | 28 | ## Submitting Pull Requests 29 | 30 | - Before you submit your PR, it's highly encouraged to run `mvn test` to make sure all tests are green. 31 | - When you are ready with your work, please use the GitHub web interface to submit your PR. 32 | - For a better workflow, please submit one PR for each issue. (You should only really work on one issue at a time) 33 | - PR title: Usually it's the same or similar to the issue title. 34 | - PR description: 35 | - Summary: Please fill in a brief summary. 36 | - Linking an issue: At the bottom, please add a closing keyword and the issue number, for example `Close #123`. [See GitHub Docs](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) 37 | - After you submitted your PR, an automated code quality check goes through on your PR content to make sure it follows the coding convention. 38 | - Then a project member will review your PR. 39 | - If your PR got approved then congratulations 🎉 your PR will be merged soon! You will be also added to the hall of fame 🚀 40 | - If changes are requested don't panic. Complete the required changes and re-submit your PR. 41 | 42 | ❗ *By not following these simple rules could lead to confusion and disappointment!* ❗ 43 | 44 | ## Project uses the following styles 45 | 46 | - Commits: Moved away from conventional commits, so use standard commit messages: 47 | - The subject line must not exceed 50 characters 48 | - The subject line should be capitalized and must not end in a period 49 | - The subject line must be written in imperative mood (Fix, not Fixed / Fixes etc.) 50 | 51 | - Indentation: Size 4 with Tabs only policy. 52 | 53 | ## Lombok 54 | 55 | The project uses Lombok. If you see lots of `method is undefined` errors in your freshly forked project that means you might need to install Lombok plugin to your IDE. 56 | 57 | #### Instructions how to install the Lombok plugin to your IDE: 58 | 59 | Eclipse: https://projectlombok.org/setup/eclipse 60 | 61 | IntelliJ IDEA: https://projectlombok.org/setup/intellij 62 | 63 | Netbeans: https://projectlombok.org/setup/netbeans 64 | 65 | Visual Studio Code: https://projectlombok.org/setup/vscode 66 | 67 | ## Thank you ♥️ 68 | 69 | Thank you for investing your time in contributing to this project! ♥️ 70 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "commitConvention": "angular", 8 | "contributors": [ 9 | { 10 | "login": "mepox", 11 | "name": "mepox", 12 | "avatar_url": "https://avatars.githubusercontent.com/u/21198248?v=4", 13 | "profile": "https://mepox.github.io/", 14 | "contributions": [ 15 | "code" 16 | ] 17 | }, 18 | { 19 | "login": "haseeb-xd", 20 | "name": "Haseeb Ansari", 21 | "avatar_url": "https://avatars.githubusercontent.com/u/47222685?v=4", 22 | "profile": "https://www.linkedin.com/in/haseebansari/", 23 | "contributions": [ 24 | "code" 25 | ] 26 | }, 27 | { 28 | "login": "isKaled", 29 | "name": "isKaled", 30 | "avatar_url": "https://avatars.githubusercontent.com/u/99230637?v=4", 31 | "profile": "https://github.com/isKaled", 32 | "contributions": [ 33 | "code" 34 | ] 35 | }, 36 | { 37 | "login": "ujwalkumar1995", 38 | "name": "Ujwal Kumar", 39 | "avatar_url": "https://avatars.githubusercontent.com/u/20976813?v=4", 40 | "profile": "https://github.com/ujwalkumar1995", 41 | "contributions": [ 42 | "code" 43 | ] 44 | }, 45 | { 46 | "login": "keer-0", 47 | "name": "Keerthi Sai Maganti", 48 | "avatar_url": "https://avatars.githubusercontent.com/u/54258313?v=4", 49 | "profile": "https://github.com/keer-0", 50 | "contributions": [ 51 | "code" 52 | ] 53 | }, 54 | { 55 | "login": "SwethaTamatam", 56 | "name": "SwethaTamatam", 57 | "avatar_url": "https://avatars.githubusercontent.com/u/109732475?v=4", 58 | "profile": "https://github.com/SwethaTamatam", 59 | "contributions": [ 60 | "code" 61 | ] 62 | }, 63 | { 64 | "login": "dmitriydb", 65 | "name": "dmitriydb", 66 | "avatar_url": "https://avatars.githubusercontent.com/u/77714869?v=4", 67 | "profile": "https://github.com/dmitriydb", 68 | "contributions": [ 69 | "code" 70 | ] 71 | }, 72 | { 73 | "login": "gaurav9777", 74 | "name": "Gaurav_mango", 75 | "avatar_url": "https://avatars.githubusercontent.com/u/62351253?v=4", 76 | "profile": "https://github.com/gaurav9777", 77 | "contributions": [ 78 | "code" 79 | ] 80 | }, 81 | { 82 | "login": "hrandhawa13", 83 | "name": "Harmanjit", 84 | "avatar_url": "https://avatars.githubusercontent.com/u/25377542?v=4", 85 | "profile": "https://github.com/hrandhawa13", 86 | "contributions": [ 87 | "code" 88 | ] 89 | }, 90 | { 91 | "login": "MaximoVincente", 92 | "name": "Maximo Vincente Mejia ", 93 | "avatar_url": "https://avatars.githubusercontent.com/u/103771906?v=4", 94 | "profile": "https://github.com/MaximoVincente", 95 | "contributions": [ 96 | "code" 97 | ] 98 | }, 99 | { 100 | "login": "mtorres6739", 101 | "name": "Mathew Torres", 102 | "avatar_url": "https://avatars.githubusercontent.com/u/104056426?v=4", 103 | "profile": "http://linkedin.com/in/mathewtorres", 104 | "contributions": [ 105 | "code" 106 | ] 107 | }, 108 | { 109 | "login": "iravimandalia", 110 | "name": "Ravi Mandalia", 111 | "avatar_url": "https://avatars.githubusercontent.com/u/28585939?v=4", 112 | "profile": "https://github.com/iravimandalia", 113 | "contributions": [ 114 | "code" 115 | ] 116 | }, 117 | { 118 | "login": "cerrussell", 119 | "name": "cerrussell", 120 | "avatar_url": "https://avatars.githubusercontent.com/u/80227828?v=4", 121 | "profile": "https://github.com/cerrussell", 122 | "contributions": [ 123 | "code" 124 | ] 125 | } 126 | ], 127 | "contributorsPerLine": 7, 128 | "skipCi": true, 129 | "repoType": "github", 130 | "repoHost": "https://github.com", 131 | "projectName": "fitbuddy", 132 | "projectOwner": "mepox" 133 | } 134 | -------------------------------------------------------------------------------- /src/test/java/app/fitbuddy/service/mapper/RoleMapperServiceTest.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.service.mapper; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.junit.jupiter.api.Assertions.*; 5 | 6 | import java.util.Collections; 7 | import java.util.List; 8 | 9 | import org.junit.jupiter.api.Nested; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | import org.mockito.InjectMocks; 13 | import org.mockito.junit.jupiter.MockitoExtension; 14 | 15 | import app.fitbuddy.dto.role.RoleRequestDTO; 16 | import app.fitbuddy.dto.role.RoleResponseDTO; 17 | import app.fitbuddy.dto.role.RoleUpdateDTO; 18 | import app.fitbuddy.entity.Role; 19 | import app.fitbuddy.testhelper.RoleTestHelper; 20 | 21 | @ExtendWith(MockitoExtension.class) 22 | class RoleMapperServiceTest { 23 | 24 | @InjectMocks 25 | RoleMapperService roleMapperService; 26 | 27 | @Nested 28 | class RequestDtoToEntity { 29 | 30 | @Test 31 | void requestDTOIsNull_returnNull() { 32 | Role actualRole = roleMapperService.requestDtoToEntity(null); 33 | 34 | assertNull(actualRole); 35 | } 36 | 37 | @Test 38 | void returnRole() { 39 | RoleRequestDTO requestDTO = new RoleRequestDTO("newRoleName"); 40 | 41 | Role actualRole = roleMapperService.requestDtoToEntity(requestDTO); 42 | 43 | assertEquals(requestDTO.getName(), actualRole.getName()); 44 | } 45 | } 46 | 47 | @Nested 48 | class EntityToResponseDto { 49 | 50 | @Test 51 | void entityIsNull_returnNull() { 52 | RoleResponseDTO actualResponseDTO = roleMapperService.entityToResponseDto(null); 53 | 54 | assertNull(actualResponseDTO); 55 | } 56 | 57 | @Test 58 | void returnResponseDTO() { 59 | Role role = RoleTestHelper.getMockRole(1, "roleName"); 60 | 61 | RoleResponseDTO actualResponseDTO = roleMapperService.entityToResponseDto(role); 62 | 63 | assertEquals(role.getId(), actualResponseDTO.getId()); 64 | assertEquals(role.getName(), actualResponseDTO.getName()); 65 | } 66 | } 67 | 68 | @Nested 69 | class EntitiesToResponseDtos { 70 | 71 | @Test 72 | void entitiesIsNull_returnEmptyList() { 73 | List actualResponseDTOs = roleMapperService.entitiesToResponseDtos(null); 74 | 75 | assertEquals(Collections.emptyList(), actualResponseDTOs); 76 | } 77 | 78 | @Test 79 | void entitiesIsEmpty_returnEmptyList() { 80 | List actualResponseDTOs = roleMapperService.entitiesToResponseDtos( 81 | Collections.emptyList()); 82 | 83 | assertEquals(Collections.emptyList(), actualResponseDTOs); 84 | } 85 | 86 | @Test 87 | void returnResponseDTOs() { 88 | Role role1 = RoleTestHelper.getMockRole(1, "roleName1"); 89 | Role role2 = RoleTestHelper.getMockRole(2, "roleName2"); 90 | List roles = List.of(role1, role2); 91 | 92 | List actualResponseDTOs = roleMapperService.entitiesToResponseDtos(roles); 93 | 94 | assertEquals(roles.size(), actualResponseDTOs.size()); 95 | assertThat(RoleTestHelper.isEqual(roles.get(0), actualResponseDTOs.get(0))).isTrue(); 96 | assertThat(RoleTestHelper.isEqual(roles.get(1), actualResponseDTOs.get(1))).isTrue(); 97 | } 98 | } 99 | 100 | @Nested 101 | class ApplyUpdateDtoToEntity { 102 | 103 | @Test 104 | void entityIsNull_returnNull() { 105 | RoleUpdateDTO updateDTO = new RoleUpdateDTO("newRoleName"); 106 | 107 | Role actualRole = roleMapperService.applyUpdateDtoToEntity(null, updateDTO); 108 | 109 | assertNull(actualRole); 110 | } 111 | 112 | @Test 113 | void updateDTOIsNull_returnNull() { 114 | Role role = RoleTestHelper.getMockRole(1, "roleName"); 115 | 116 | Role actualRole = roleMapperService.applyUpdateDtoToEntity(role, null); 117 | 118 | assertNull(actualRole); 119 | } 120 | 121 | @Test 122 | void returnUpdatedExercise() { 123 | Role role = RoleTestHelper.getMockRole(1, "roleName"); 124 | RoleUpdateDTO updateDTO = new RoleUpdateDTO("newRoleName"); 125 | 126 | Role actualRole = roleMapperService.applyUpdateDtoToEntity(role, updateDTO); 127 | 128 | assertEquals(updateDTO.getName(), actualRole.getName()); 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/resources/static/user/js/exercise.js: -------------------------------------------------------------------------------- 1 | const EXERCISES_API_PATH = "/user/exercises"; 2 | 3 | function showExercises() { 4 | let xhr = new XMLHttpRequest(); 5 | xhr.open("GET", EXERCISES_API_PATH); 6 | xhr.send(); 7 | 8 | xhr.onreadystatechange = function() { 9 | if (this.readyState == XMLHttpRequest.DONE) { 10 | if (this.status == 200) { 11 | // SUCCESS 12 | let data = JSON.parse(this.responseText); 13 | let tbody = document.getElementById("exercise-table").getElementsByTagName("tbody")[0]; 14 | 15 | while (tbody.firstChild) { 16 | tbody.removeChild(tbody.firstChild); 17 | } 18 | 19 | let add = ""; 20 | 21 | for (let i = 0; i < data.length; i++) { 22 | let exerciseId = data[i].id; 23 | let exerciseNameId = "exercise-name-" + exerciseId; 24 | let actionsId = "exercise-actions-" + exerciseId; 25 | add += "" + (i+1) + "" + 26 | "" + data[i].name + "" + 27 | "" + 28 | "" + 29 | "" + 30 | ""; 31 | } 32 | 33 | tbody.innerHTML = add; 34 | } else { 35 | // ERROR 36 | console.log("ERROR: " + this.responseText); 37 | } 38 | } 39 | }; 40 | } 41 | 42 | function deleteExercise(exerciseId) { 43 | hideStatus(); 44 | let xhr = new XMLHttpRequest(); 45 | xhr.open("DELETE", EXERCISES_API_PATH + "/" + exerciseId); 46 | xhr.send(); 47 | 48 | xhr.onreadystatechange = function() { 49 | if (this.readyState == XMLHttpRequest.DONE) { 50 | if (this.status == 200) { 51 | // SUCCESS 52 | console.log("OK: " + this.responseText); 53 | } else { 54 | // ERROR 55 | console.log("ERROR: " + this.responseText); 56 | showStatus(this.responseText); 57 | } 58 | showExercises(); 59 | } 60 | } 61 | } 62 | 63 | function onAddExercise() { 64 | hideStatus(); 65 | let name = document.forms["new-exercise-form"]["name"].value; 66 | name = name.trim(); 67 | 68 | let data = { "name" : name }; 69 | 70 | let xhr = new XMLHttpRequest(); 71 | xhr.open("POST", EXERCISES_API_PATH); 72 | xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); 73 | xhr.send(JSON.stringify(data)); 74 | 75 | xhr.onreadystatechange = function() { 76 | if (this.readyState == XMLHttpRequest.DONE) { 77 | if (this.status == 200) { 78 | // SUCCESS 79 | console.log("OK: " + this.responseText); 80 | } else { 81 | // ERROR 82 | console.log("ERROR: " + this.responseText); 83 | showStatus(this.responseText); 84 | } 85 | showExercises(); 86 | } 87 | } 88 | 89 | document.forms["new-exercise-form"]["name"].value = ""; 90 | } 91 | 92 | function editExercise(exerciseId) { 93 | let exerciseNameElement = document.getElementById("exercise-name-" + exerciseId); 94 | let actionsElement = document.getElementById("exercise-actions-" + exerciseId); 95 | // read exercise name 96 | let exerciseName = exerciseNameElement.textContent; 97 | // add input field 98 | exerciseNameElement.innerHTML = ""; 99 | // remove edit button 100 | actionsElement.removeChild(actionsElement.firstChild); 101 | // add save button 102 | actionsElement.innerHTML = "" + 103 | actionsElement.innerHTML; 104 | } 105 | 106 | function saveExercise(exerciseId) { 107 | hideStatus(); 108 | // read the exercise name 109 | let exerciseNameElement = document.getElementById("exercise-name-" + exerciseId); 110 | let exerciseName = exerciseNameElement.firstChild.value; 111 | exerciseName = exerciseName.trim(); 112 | 113 | let data = { "name" : exerciseName }; 114 | 115 | let xhr = new XMLHttpRequest(); 116 | xhr.open("PUT", EXERCISES_API_PATH + "/" + exerciseId); 117 | xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); 118 | xhr.send(JSON.stringify(data)); 119 | 120 | xhr.onreadystatechange = function() { 121 | if (this.readyState == XMLHttpRequest.DONE) { 122 | if (this.status == 200) { 123 | // SUCCESS 124 | console.log("OK: " + this.responseText); 125 | } else { 126 | // ERROR 127 | console.log("ERROR: " + this.responseText); 128 | showStatus(this.responseText); 129 | } 130 | showExercises(); 131 | } 132 | } 133 | } -------------------------------------------------------------------------------- /src/test/java/app/fitbuddy/service/crud/RoleCrudServiceTest.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.service.crud; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | import static org.mockito.ArgumentMatchers.any; 5 | import static org.mockito.ArgumentMatchers.anyInt; 6 | import static org.mockito.ArgumentMatchers.anyString; 7 | import static org.mockito.Mockito.verify; 8 | import static org.mockito.Mockito.when; 9 | 10 | import java.util.Optional; 11 | 12 | import org.junit.jupiter.api.Nested; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.api.extension.ExtendWith; 15 | import org.mockito.InjectMocks; 16 | import org.mockito.Mock; 17 | import org.mockito.junit.jupiter.MockitoExtension; 18 | 19 | import app.fitbuddy.dto.role.RoleRequestDTO; 20 | import app.fitbuddy.dto.role.RoleUpdateDTO; 21 | import app.fitbuddy.entity.Role; 22 | import app.fitbuddy.exception.FitBuddyException; 23 | import app.fitbuddy.repository.RoleRepository; 24 | import app.fitbuddy.service.mapper.RoleMapperService; 25 | import app.fitbuddy.testhelper.RoleTestHelper; 26 | 27 | @ExtendWith(MockitoExtension.class) 28 | class RoleCrudServiceTest { 29 | 30 | @InjectMocks 31 | RoleCrudService roleCrudService; 32 | 33 | @Mock 34 | RoleRepository roleRepository; 35 | 36 | @Mock 37 | RoleMapperService roleMapperService; 38 | 39 | @Nested 40 | class Create { 41 | 42 | @Test 43 | void requestDTOIsNull_returnNull() { 44 | assertNull(roleCrudService.create(null)); 45 | } 46 | 47 | @Test 48 | void nameAlreadyExists_throwFitBuddyException() { 49 | RoleRequestDTO requestDTO = new RoleRequestDTO("roleName"); 50 | Role role = RoleTestHelper.getMockRole(); 51 | 52 | when(roleRepository.findByName(anyString())).thenReturn(Optional.of(role)); 53 | 54 | assertThrows(FitBuddyException.class, () -> roleCrudService.create(requestDTO)); 55 | } 56 | 57 | @Test 58 | void callSave() { 59 | RoleRequestDTO requestDTO = new RoleRequestDTO("roleName"); 60 | Role role = RoleTestHelper.getMockRole(); 61 | 62 | when(roleRepository.findByName(anyString())).thenReturn(Optional.empty()); 63 | when(roleMapperService.requestDtoToEntity(requestDTO)).thenReturn(role); 64 | 65 | roleCrudService.create(requestDTO); 66 | 67 | verify(roleRepository).save(role); 68 | } 69 | } 70 | 71 | @Nested 72 | class ReadById { 73 | 74 | @Test 75 | void notFoundById_returnNull() { 76 | when(roleRepository.findById(anyInt())).thenReturn(Optional.empty()); 77 | 78 | assertNull(roleCrudService.readById(1)); 79 | } 80 | } 81 | 82 | @Nested 83 | class ReadByName { 84 | 85 | @Test 86 | void notFoundByName_returnNull() { 87 | when(roleRepository.findByName(anyString())).thenReturn(Optional.empty()); 88 | 89 | assertNull(roleCrudService.readByName("roleName")); 90 | } 91 | } 92 | 93 | @Nested 94 | class ReadAll { 95 | 96 | @Test 97 | void callAppUserRepository() { 98 | roleCrudService.readAll(); 99 | 100 | verify(roleRepository).findAll(); 101 | } 102 | } 103 | 104 | @Nested 105 | class Update { 106 | 107 | @Test 108 | void updateDTOIsNull_returnNull() { 109 | assertNull(roleCrudService.update(1, null)); 110 | } 111 | 112 | @Test 113 | void existingRoleNotFound_returnNull() { 114 | RoleUpdateDTO updateDTO = new RoleUpdateDTO("newRoleName"); 115 | 116 | when(roleRepository.findById(anyInt())).thenReturn(Optional.empty()); 117 | 118 | assertNull(roleCrudService.update(1, updateDTO)); 119 | } 120 | 121 | @Test 122 | void nameAlreadyExists_throwFitBuddyException() { 123 | RoleUpdateDTO updateDTO = new RoleUpdateDTO("newRoleName"); 124 | Role role = RoleTestHelper.getMockRole(); 125 | 126 | when(roleRepository.findById(anyInt())).thenReturn(Optional.of(role)); 127 | when(roleRepository.findByName(anyString())).thenReturn(Optional.of(role)); 128 | 129 | assertThrows(FitBuddyException.class, () -> roleCrudService.update(1, updateDTO)); 130 | } 131 | 132 | @Test 133 | void applyAndSave() { 134 | RoleUpdateDTO updateDTO = new RoleUpdateDTO("newRoleName"); 135 | Role role = RoleTestHelper.getMockRole(); 136 | 137 | when(roleRepository.findById(anyInt())).thenReturn(Optional.of(role)); 138 | when(roleRepository.findByName(anyString())).thenReturn(Optional.empty()); 139 | when(roleMapperService.applyUpdateDtoToEntity(any(Role.class), any(RoleUpdateDTO.class))).thenReturn(role); 140 | 141 | roleCrudService.update(1, updateDTO); 142 | 143 | verify(roleMapperService).applyUpdateDtoToEntity(role, updateDTO); 144 | verify(roleRepository).save(role); 145 | } 146 | } 147 | 148 | @Nested 149 | class Delete { 150 | 151 | @Test 152 | void callDeleteById() { 153 | roleCrudService.delete(111); 154 | 155 | verify(roleRepository).deleteById(111); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/main/java/app/fitbuddy/controller/crud/HistoryController.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.controller.crud; 2 | 3 | import java.time.LocalDate; 4 | import java.time.format.DateTimeParseException; 5 | import java.util.List; 6 | import java.util.Collections; 7 | 8 | import javax.validation.Valid; 9 | import javax.validation.constraints.NotNull; 10 | 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.security.access.prepost.PreAuthorize; 15 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 16 | import org.springframework.web.bind.annotation.DeleteMapping; 17 | import org.springframework.web.bind.annotation.GetMapping; 18 | import org.springframework.web.bind.annotation.PathVariable; 19 | import org.springframework.web.bind.annotation.PostMapping; 20 | import org.springframework.web.bind.annotation.PutMapping; 21 | import org.springframework.web.bind.annotation.RequestBody; 22 | import org.springframework.web.bind.annotation.RequestMapping; 23 | import org.springframework.web.bind.annotation.RestController; 24 | 25 | import app.fitbuddy.dto.history.HistoryRequestDTO; 26 | import app.fitbuddy.dto.history.HistoryResponseDTO; 27 | import app.fitbuddy.dto.history.HistoryUpdateDTO; 28 | import app.fitbuddy.exception.FitBuddyException; 29 | import app.fitbuddy.security.AppUserPrincipal; 30 | import app.fitbuddy.service.crud.HistoryCrudService; 31 | 32 | @RestController 33 | @RequestMapping("/user/history") 34 | @PreAuthorize("authenticated") 35 | public class HistoryController { 36 | 37 | private final Logger logger; 38 | private final HistoryCrudService historyCrudService; 39 | private final String DATE_NOT_VALID = "Date is not valid"; 40 | 41 | @Autowired 42 | public HistoryController(HistoryCrudService historyCrudService) { 43 | this.historyCrudService = historyCrudService; 44 | this.logger = LoggerFactory.getLogger(HistoryController.class); 45 | } 46 | 47 | @PostMapping 48 | public void create(@Valid @RequestBody HistoryRequestDTO historyRequestDTO, 49 | @AuthenticationPrincipal AppUserPrincipal appUserPrincipal) { 50 | Integer userId = appUserPrincipal.getId(); 51 | if (userId != null) { 52 | historyRequestDTO.setAppUserId(userId); 53 | historyCrudService.create(historyRequestDTO); 54 | logger.info("Creating new history: {}", historyRequestDTO); 55 | } 56 | } 57 | 58 | @GetMapping("{date}") 59 | public List readAll(@PathVariable("date") String strDate, 60 | @AuthenticationPrincipal AppUserPrincipal appUserPrincipal) { 61 | if (strDate != null) { 62 | try { 63 | LocalDate.parse(strDate); 64 | } catch (DateTimeParseException e) { 65 | throw new FitBuddyException(DATE_NOT_VALID); 66 | } 67 | Integer userId = appUserPrincipal.getId(); 68 | if (userId != null) { 69 | List historyResponseDTOs = historyCrudService.readMany(userId, strDate); 70 | logger.info("Sending a history for: {}", strDate); 71 | return historyResponseDTOs; 72 | } 73 | } 74 | return Collections.emptyList(); 75 | } 76 | 77 | @PutMapping("{id}") 78 | public void update(@PathVariable("id") @NotNull Integer historyId, 79 | @Valid @RequestBody HistoryUpdateDTO historyUpdateDTO, 80 | @AuthenticationPrincipal AppUserPrincipal appUserPrincipal) { 81 | Integer userId = appUserPrincipal.getId(); 82 | if (userId != null) { 83 | HistoryResponseDTO historyResponseDTO = historyCrudService.readById(historyId); 84 | if (historyResponseDTO != null && historyResponseDTO.getAppUserId().equals(userId)) { 85 | historyCrudService.update(historyId, historyUpdateDTO); 86 | logger.info("Updating history: {}", historyUpdateDTO); 87 | } else { 88 | throw new FitBuddyException("UserIds doesn't match. Cannot update others History."); 89 | } 90 | } 91 | } 92 | 93 | @DeleteMapping("{id}") 94 | public void delete(@PathVariable("id") @NotNull Integer historyId, 95 | @AuthenticationPrincipal AppUserPrincipal appUserPrincipal) { 96 | Integer userId = appUserPrincipal.getId(); 97 | if (userId != null) { 98 | HistoryResponseDTO historyResponseDTO = historyCrudService.readById(historyId); 99 | if (historyResponseDTO != null && historyResponseDTO.getAppUserId().equals(userId)) { 100 | historyCrudService.delete(historyId); 101 | logger.info("Deleting history: {}", historyResponseDTO); 102 | } else { 103 | throw new FitBuddyException("UserIds doesn't match. Cannot delete others History."); 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/test/java/app/fitbuddy/service/crud/ExerciseCrudServiceTest.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.service.crud; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | import static org.mockito.ArgumentMatchers.any; 5 | import static org.mockito.ArgumentMatchers.anyInt; 6 | import static org.mockito.ArgumentMatchers.anyString; 7 | import static org.mockito.Mockito.verify; 8 | import static org.mockito.Mockito.when; 9 | 10 | import java.util.Optional; 11 | 12 | import org.junit.jupiter.api.Nested; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.api.extension.ExtendWith; 15 | import org.mockito.InjectMocks; 16 | import org.mockito.Mock; 17 | import org.mockito.junit.jupiter.MockitoExtension; 18 | 19 | import app.fitbuddy.dto.exercise.ExerciseRequestDTO; 20 | import app.fitbuddy.dto.exercise.ExerciseUpdateDTO; 21 | import app.fitbuddy.entity.Exercise; 22 | import app.fitbuddy.exception.FitBuddyException; 23 | import app.fitbuddy.repository.ExerciseRepository; 24 | import app.fitbuddy.service.mapper.ExerciseMapperService; 25 | import app.fitbuddy.testhelper.ExerciseTestHelper; 26 | 27 | @ExtendWith(MockitoExtension.class) 28 | class ExerciseCrudServiceTest { 29 | 30 | @InjectMocks 31 | ExerciseCrudService exerciseCrudService; 32 | 33 | @Mock 34 | ExerciseRepository exerciseRepository; 35 | 36 | @Mock 37 | ExerciseMapperService exerciseMapperService; 38 | 39 | @Nested 40 | class Create { 41 | 42 | @Test 43 | void requestDTOIsNull_returnNull() { 44 | assertNull(exerciseCrudService.create(null)); 45 | } 46 | 47 | @Test 48 | void nameAlreadyExists_throwFitBuddyException() { 49 | ExerciseRequestDTO requestDTO = new ExerciseRequestDTO("exerciseName", 11); 50 | Exercise exercise = ExerciseTestHelper.getMockExercise(); 51 | 52 | when(exerciseRepository.findByNameAndUserId(anyString(), anyInt())).thenReturn(Optional.of(exercise)); 53 | 54 | assertThrows(FitBuddyException.class, () -> exerciseCrudService.create(requestDTO)); 55 | } 56 | 57 | @Test 58 | void callSave() { 59 | ExerciseRequestDTO requestDTO = new ExerciseRequestDTO("exerciseName", 11); 60 | Exercise exercise = ExerciseTestHelper.getMockExercise(); 61 | 62 | when(exerciseRepository.findByNameAndUserId(anyString(), anyInt())).thenReturn(Optional.empty()); 63 | when(exerciseMapperService.requestDtoToEntity(requestDTO)).thenReturn(exercise); 64 | 65 | exerciseCrudService.create(requestDTO); 66 | 67 | verify(exerciseRepository).save(exercise); 68 | } 69 | } 70 | 71 | @Nested 72 | class ReadById { 73 | 74 | @Test 75 | void notFoundById_returnNull() { 76 | when(exerciseRepository.findById(anyInt())).thenReturn(Optional.empty()); 77 | 78 | assertNull(exerciseCrudService.readById(1)); 79 | } 80 | } 81 | 82 | @Nested 83 | class ReadMany { 84 | 85 | @Test 86 | void callExerciseRepository() { 87 | exerciseCrudService.readMany(11); 88 | 89 | verify(exerciseRepository).findAllByUserId(11); 90 | } 91 | } 92 | 93 | @Nested 94 | class Update { 95 | 96 | @Test 97 | void updateDTOIsNull_returnNull() { 98 | assertNull(exerciseCrudService.update(1, null)); 99 | } 100 | 101 | @Test 102 | void existingExerciseNotFound_returnNull() { 103 | ExerciseUpdateDTO updateDTO = new ExerciseUpdateDTO("newExerciseName"); 104 | 105 | when(exerciseRepository.findById(anyInt())).thenReturn(Optional.empty()); 106 | 107 | assertNull(exerciseCrudService.update(1, updateDTO)); 108 | } 109 | 110 | @Test 111 | void nameAlreadyExists_throwFitBuddyException() { 112 | ExerciseUpdateDTO updateDTO = new ExerciseUpdateDTO("newExerciseName"); 113 | Exercise exercise = ExerciseTestHelper.getMockExercise(); 114 | 115 | when(exerciseRepository.findById(anyInt())).thenReturn(Optional.of(exercise)); 116 | when(exerciseRepository.findByName(anyString())).thenReturn(Optional.of(exercise)); 117 | 118 | assertThrows(FitBuddyException.class, () -> exerciseCrudService.update(1, updateDTO)); 119 | } 120 | 121 | @Test 122 | void applyAndSave() { 123 | ExerciseUpdateDTO updateDTO = new ExerciseUpdateDTO("newExerciseName"); 124 | Exercise exercise = ExerciseTestHelper.getMockExercise(); 125 | 126 | when(exerciseRepository.findById(anyInt())).thenReturn(Optional.of(exercise)); 127 | when(exerciseRepository.findByName(anyString())).thenReturn(Optional.empty()); 128 | when(exerciseMapperService.applyUpdateDtoToEntity(any(Exercise.class), 129 | any(ExerciseUpdateDTO.class))).thenReturn(exercise); 130 | 131 | exerciseCrudService.update(1, updateDTO); 132 | 133 | verify(exerciseMapperService).applyUpdateDtoToEntity(exercise, updateDTO); 134 | verify(exerciseRepository).save(exercise); 135 | } 136 | } 137 | 138 | @Nested 139 | class Delete { 140 | 141 | @Test 142 | void callDeleteById() { 143 | exerciseCrudService.delete(111); 144 | 145 | verify(exerciseRepository).deleteById(111); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/test/java/app/fitbuddy/service/crud/AppUserCrudServiceTest.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.service.crud; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | import static org.mockito.ArgumentMatchers.any; 5 | import static org.mockito.ArgumentMatchers.anyInt; 6 | import static org.mockito.ArgumentMatchers.anyString; 7 | import static org.mockito.Mockito.verify; 8 | import static org.mockito.Mockito.when; 9 | 10 | import java.util.Optional; 11 | 12 | import org.junit.jupiter.api.Nested; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.api.extension.ExtendWith; 15 | import org.mockito.InjectMocks; 16 | import org.mockito.Mock; 17 | import org.mockito.junit.jupiter.MockitoExtension; 18 | 19 | import app.fitbuddy.dto.appuser.AppUserRequestDTO; 20 | import app.fitbuddy.repository.AppUserRepository; 21 | import app.fitbuddy.service.mapper.AppUserMapperService; 22 | import app.fitbuddy.dto.appuser.AppUserUpdateDTO; 23 | import app.fitbuddy.entity.AppUser; 24 | import app.fitbuddy.exception.FitBuddyException; 25 | import app.fitbuddy.testhelper.AppUserTestHelper; 26 | 27 | @ExtendWith(MockitoExtension.class) 28 | class AppUserCrudServiceTest { 29 | 30 | @InjectMocks 31 | AppUserCrudService appUserCrudService; 32 | 33 | @Mock 34 | AppUserRepository appUserRepository; 35 | 36 | @Mock 37 | AppUserMapperService appUserMapperService; 38 | 39 | @Nested 40 | class Create { 41 | 42 | @Test 43 | void requestDTOIsNull_returnNull() { 44 | assertNull(appUserCrudService.create(null)); 45 | } 46 | 47 | @Test 48 | void nameAlreadyExists_throwFitBuddyException() { 49 | AppUserRequestDTO appUserRequestDTO = new AppUserRequestDTO("name", "password", "roleName"); 50 | AppUser appUser = AppUserTestHelper.getMockAppUser(); 51 | 52 | when(appUserRepository.findByName(anyString())).thenReturn(Optional.of(appUser)); 53 | 54 | assertThrows(FitBuddyException.class, () -> appUserCrudService.create(appUserRequestDTO)); 55 | } 56 | 57 | @Test 58 | void callSave() { 59 | AppUserRequestDTO appUserRequestDTO = new AppUserRequestDTO("name", "password", "roleName"); 60 | AppUser appUser = AppUserTestHelper.getMockAppUser(); 61 | 62 | when(appUserRepository.findByName(anyString())).thenReturn(Optional.empty()); 63 | when(appUserMapperService.requestDtoToEntity(appUserRequestDTO)).thenReturn(appUser); 64 | 65 | appUserCrudService.create(appUserRequestDTO); 66 | 67 | verify(appUserRepository).save(appUser); 68 | } 69 | } 70 | 71 | @Nested 72 | class ReadById { 73 | 74 | @Test 75 | void notFoundById_returnNull() { 76 | when(appUserRepository.findById(anyInt())).thenReturn(Optional.empty()); 77 | 78 | assertNull(appUserCrudService.readById(1)); 79 | } 80 | } 81 | 82 | @Nested 83 | class ReadByName { 84 | 85 | @Test 86 | void notFoundByName_returnNull() { 87 | when(appUserRepository.findByName(anyString())).thenReturn(Optional.empty()); 88 | 89 | assertNull(appUserCrudService.readByName("name")); 90 | } 91 | } 92 | 93 | @Nested 94 | class ReadAll { 95 | 96 | @Test 97 | void callAppUserRepository() { 98 | appUserCrudService.readAll(); 99 | 100 | verify(appUserRepository).findAll(); 101 | } 102 | } 103 | 104 | @Nested 105 | class Update { 106 | 107 | @Test 108 | void updateDTOIsNull_returnNull() { 109 | assertNull(appUserCrudService.update(1, null)); 110 | } 111 | 112 | @Test 113 | void existingAppUserNotFound_returnNull() { 114 | AppUserUpdateDTO updateDTO = new AppUserUpdateDTO("name", "roleName"); 115 | 116 | when(appUserRepository.findById(anyInt())).thenReturn(Optional.empty()); 117 | 118 | assertNull(appUserCrudService.update(1, updateDTO)); 119 | } 120 | 121 | @Test 122 | void nameAlreadyExists_throwFitBuddyException() { 123 | AppUserUpdateDTO updateDTO = new AppUserUpdateDTO("name", "roleName"); 124 | AppUser appUser = AppUserTestHelper.getMockAppUser(); 125 | 126 | when(appUserRepository.findById(anyInt())).thenReturn(Optional.of(appUser)); 127 | when(appUserRepository.findByName(anyString())).thenReturn(Optional.of(appUser)); 128 | 129 | assertThrows(FitBuddyException.class, () -> appUserCrudService.update(1, updateDTO)); 130 | } 131 | 132 | @Test 133 | void applyAndSave() { 134 | AppUserUpdateDTO updateDTO = new AppUserUpdateDTO("name", "roleName"); 135 | AppUser appUser = AppUserTestHelper.getMockAppUser(); 136 | 137 | when(appUserRepository.findById(anyInt())).thenReturn(Optional.of(appUser)); 138 | when(appUserRepository.findByName(anyString())).thenReturn(Optional.empty()); 139 | when(appUserMapperService.applyUpdateDtoToEntity(any(AppUser.class), 140 | any(AppUserUpdateDTO.class))).thenReturn(appUser); 141 | 142 | appUserCrudService.update(1, updateDTO); 143 | 144 | verify(appUserMapperService).applyUpdateDtoToEntity(appUser, updateDTO); 145 | verify(appUserRepository).save(appUser); 146 | } 147 | } 148 | 149 | @Nested 150 | class Delete { 151 | 152 | @Test 153 | void callDeleteById() { 154 | appUserCrudService.delete(111); 155 | 156 | verify(appUserRepository).deleteById(111); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/test/java/app/fitbuddy/service/mapper/ExerciseMapperServiceTest.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.service.mapper; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.junit.jupiter.api.Assertions.*; 5 | import static org.mockito.ArgumentMatchers.anyInt; 6 | import static org.mockito.Mockito.when; 7 | 8 | import java.util.Collections; 9 | import java.util.List; 10 | import java.util.Optional; 11 | 12 | import org.junit.jupiter.api.Nested; 13 | import org.junit.jupiter.api.Test; 14 | import org.junit.jupiter.api.extension.ExtendWith; 15 | import org.mockito.InjectMocks; 16 | import org.mockito.Mock; 17 | import org.mockito.junit.jupiter.MockitoExtension; 18 | 19 | import app.fitbuddy.dto.exercise.ExerciseRequestDTO; 20 | import app.fitbuddy.dto.exercise.ExerciseResponseDTO; 21 | import app.fitbuddy.dto.exercise.ExerciseUpdateDTO; 22 | import app.fitbuddy.entity.AppUser; 23 | import app.fitbuddy.entity.Exercise; 24 | import app.fitbuddy.exception.FitBuddyException; 25 | import app.fitbuddy.repository.AppUserRepository; 26 | import app.fitbuddy.testhelper.AppUserTestHelper; 27 | import app.fitbuddy.testhelper.ExerciseTestHelper; 28 | 29 | @ExtendWith(MockitoExtension.class) 30 | class ExerciseMapperServiceTest { 31 | 32 | @InjectMocks 33 | ExerciseMapperService exerciseMapperService; 34 | 35 | @Mock 36 | AppUserRepository appUserRepository; 37 | 38 | @Nested 39 | class RequestDtoToEntity { 40 | 41 | @Test 42 | void requestDTOIsNull_returnNull() { 43 | Exercise actualExercise = exerciseMapperService.requestDtoToEntity(null); 44 | 45 | assertNull(actualExercise); 46 | } 47 | 48 | @Test 49 | void appUserNotFound_throwFitBuddyException() { 50 | ExerciseRequestDTO requestDTO = new ExerciseRequestDTO("name", 1); 51 | 52 | when(appUserRepository.findById(anyInt())).thenReturn(Optional.empty()); 53 | 54 | assertThrows(FitBuddyException.class, () -> exerciseMapperService.requestDtoToEntity(requestDTO)); 55 | } 56 | 57 | @Test 58 | void returnExercise() { 59 | ExerciseRequestDTO requestDTO = new ExerciseRequestDTO("name", 1); 60 | AppUser appUser = AppUserTestHelper.getMockAppUser(1, "name", "password"); 61 | 62 | when(appUserRepository.findById(anyInt())).thenReturn(Optional.of(appUser)); 63 | 64 | Exercise actualExercise = exerciseMapperService.requestDtoToEntity(requestDTO); 65 | 66 | assertEquals(requestDTO.getName(), actualExercise.getName()); 67 | assertEquals(requestDTO.getAppUserId(), actualExercise.getAppUser().getId()); 68 | } 69 | } 70 | 71 | @Nested 72 | class EntityToResponseDto { 73 | 74 | @Test 75 | void entityIsNull_returnNull() { 76 | ExerciseResponseDTO actualResponseDTO = exerciseMapperService.entityToResponseDto(null); 77 | 78 | assertNull(actualResponseDTO); 79 | } 80 | 81 | @Test 82 | void returnResponseDTO() { 83 | AppUser appUser = AppUserTestHelper.getMockAppUser(1, "name", "password"); 84 | Exercise exercise = ExerciseTestHelper.getMockExercise(11, "exerciseName", appUser); 85 | 86 | ExerciseResponseDTO actualResponseDTO = exerciseMapperService.entityToResponseDto(exercise); 87 | 88 | assertEquals(exercise.getId(), actualResponseDTO.getId()); 89 | assertEquals(exercise.getName(), actualResponseDTO.getName()); 90 | assertEquals(exercise.getAppUser().getId(), actualResponseDTO.getAppUserId()); 91 | } 92 | } 93 | 94 | @Nested 95 | class EntitiesToResponseDtos { 96 | 97 | @Test 98 | void entitiesIsNull_returnEmptyList() { 99 | List actualResponseDTOs = exerciseMapperService.entitiesToResponseDtos(null); 100 | 101 | assertEquals(Collections.emptyList(), actualResponseDTOs); 102 | } 103 | 104 | @Test 105 | void entitiesIsEmpty_returnEmptyList() { 106 | List actualResponseDTOs = exerciseMapperService.entitiesToResponseDtos( 107 | Collections.emptyList()); 108 | 109 | assertEquals(Collections.emptyList(), actualResponseDTOs); 110 | } 111 | 112 | @Test 113 | void returnResponseDTOs() { 114 | AppUser appUser = AppUserTestHelper.getMockAppUser(1, "name", "password"); 115 | Exercise exercise1 = ExerciseTestHelper.getMockExercise(11, "exerciseName", appUser); 116 | Exercise exercise2 = ExerciseTestHelper.getMockExercise(22, "exerciseName", appUser); 117 | List exercises = List.of(exercise1, exercise2); 118 | 119 | List actualResponseDTOs = exerciseMapperService.entitiesToResponseDtos(exercises); 120 | 121 | assertEquals(exercises.size(), actualResponseDTOs.size()); 122 | assertThat(ExerciseTestHelper.isEqual(exercises.get(0), actualResponseDTOs.get(0))).isTrue(); 123 | assertThat(ExerciseTestHelper.isEqual(exercises.get(1), actualResponseDTOs.get(1))).isTrue(); 124 | } 125 | } 126 | 127 | @Nested 128 | class ApplyUpdateDtoToEntity { 129 | 130 | @Test 131 | void entityIsNull_returnNull() { 132 | ExerciseUpdateDTO updateDTO = new ExerciseUpdateDTO("exerciseName"); 133 | 134 | Exercise actualExercise = exerciseMapperService.applyUpdateDtoToEntity(null, updateDTO); 135 | 136 | assertNull(actualExercise); 137 | } 138 | 139 | @Test 140 | void updateDTOIsNull_returnNull() { 141 | Exercise exercise = ExerciseTestHelper.getMockExercise(11, "exerciseName"); 142 | 143 | Exercise actualExercise = exerciseMapperService.applyUpdateDtoToEntity(exercise, null); 144 | 145 | assertNull(actualExercise); 146 | } 147 | 148 | @Test 149 | void returnUpdatedExercise() { 150 | Exercise exercise = ExerciseTestHelper.getMockExercise(11, "exerciseName"); 151 | ExerciseUpdateDTO updateDTO = new ExerciseUpdateDTO("newExerciseName"); 152 | 153 | Exercise actualExercise = exerciseMapperService.applyUpdateDtoToEntity(exercise, updateDTO); 154 | 155 | assertEquals(updateDTO.getName(), actualExercise.getName()); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/test/java/app/fitbuddy/service/mapper/AppUserMapperServiceTest.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.service.mapper; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.junit.jupiter.api.Assertions.*; 5 | import static org.mockito.ArgumentMatchers.anyString; 6 | import static org.mockito.Mockito.verify; 7 | import static org.mockito.Mockito.when; 8 | 9 | import app.fitbuddy.dto.appuser.AppUserRequestDTO; 10 | import app.fitbuddy.dto.appuser.AppUserResponseDTO; 11 | import app.fitbuddy.dto.appuser.AppUserUpdateDTO; 12 | import app.fitbuddy.entity.AppUser; 13 | import app.fitbuddy.entity.Role; 14 | import app.fitbuddy.exception.FitBuddyException; 15 | 16 | import org.junit.jupiter.api.Nested; 17 | import org.junit.jupiter.api.Test; 18 | import org.junit.jupiter.api.extension.ExtendWith; 19 | import org.mockito.InjectMocks; 20 | import org.mockito.Mock; 21 | import org.mockito.junit.jupiter.MockitoExtension; 22 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 23 | 24 | import app.fitbuddy.repository.RoleRepository; 25 | import app.fitbuddy.testhelper.AppUserTestHelper; 26 | import app.fitbuddy.testhelper.RoleTestHelper; 27 | 28 | import java.util.Collections; 29 | import java.util.List; 30 | import java.util.Optional; 31 | 32 | @ExtendWith(MockitoExtension.class) 33 | class AppUserMapperServiceTest { 34 | 35 | @InjectMocks 36 | AppUserMapperService appUserMapperService; 37 | 38 | @Mock 39 | RoleRepository roleRepository; 40 | 41 | @Mock 42 | BCryptPasswordEncoder bCryptPasswordEncoder; 43 | 44 | @Nested 45 | class RequestDtoToEntity { 46 | 47 | @Test 48 | void requestDTOIsNull_returnNull() { 49 | AppUser actualAppUser = appUserMapperService.requestDtoToEntity(null); 50 | 51 | assertNull(actualAppUser); 52 | } 53 | 54 | @Test 55 | void roleNotFound_throwFitBuddyException() { 56 | AppUserRequestDTO requestDTO = new AppUserRequestDTO("name", "password", "roleName"); 57 | 58 | when(roleRepository.findByName(anyString())).thenReturn(Optional.empty()); 59 | 60 | assertThrows(FitBuddyException.class, () -> appUserMapperService.requestDtoToEntity(requestDTO)); 61 | } 62 | 63 | @Test 64 | void returnAppUser() { 65 | AppUserRequestDTO requestDTO = new AppUserRequestDTO("name", "password", "roleName"); 66 | Role role = RoleTestHelper.getMockRole(1, "roleName"); 67 | 68 | when(roleRepository.findByName(anyString())).thenReturn(Optional.of(role)); 69 | when(bCryptPasswordEncoder.encode(anyString())).thenReturn("encodedPassword"); 70 | 71 | AppUser actualAppUser = appUserMapperService.requestDtoToEntity(requestDTO); 72 | 73 | assertEquals(requestDTO.getName(), actualAppUser.getName()); 74 | assertEquals("encodedPassword", actualAppUser.getPassword()); 75 | assertEquals(requestDTO.getRolename(), actualAppUser.getRole().getName()); 76 | verify(bCryptPasswordEncoder).encode(requestDTO.getPassword()); 77 | } 78 | } 79 | 80 | @Nested 81 | class EntityToResponseDto { 82 | 83 | @Test 84 | void entityIsNull_returnNull() { 85 | AppUserResponseDTO actualResponseDTO = appUserMapperService.entityToResponseDto(null); 86 | 87 | assertNull(actualResponseDTO); 88 | } 89 | 90 | @Test 91 | void returnResponseDTO() { 92 | AppUser appUser = AppUserTestHelper.getMockAppUser(1, "name", "password"); 93 | 94 | AppUserResponseDTO actualResponseDTO = appUserMapperService.entityToResponseDto(appUser); 95 | 96 | assertThat(AppUserTestHelper.isEqual(appUser, actualResponseDTO)).isTrue(); 97 | } 98 | } 99 | 100 | @Nested 101 | class EntitiesToResponseDtos { 102 | 103 | @Test 104 | void entitiesIsNull_returnEmptyList() { 105 | List actualResponseDTOs = appUserMapperService.entitiesToResponseDtos(null); 106 | 107 | assertEquals(Collections.emptyList(), actualResponseDTOs); 108 | } 109 | 110 | @Test 111 | void entitiesIsEmpty_returnEmptyList() { 112 | List actualResponseDTOs = appUserMapperService.entitiesToResponseDtos( 113 | Collections.emptyList()); 114 | 115 | assertEquals(Collections.emptyList(), actualResponseDTOs); 116 | } 117 | 118 | @Test 119 | void returnResponseDTOs() { 120 | AppUser appUser1 = AppUserTestHelper.getMockAppUser(1, "name1", "password1"); 121 | AppUser appUser2 = AppUserTestHelper.getMockAppUser(2, "name2", "password2"); 122 | List appUsers = List.of(appUser1, appUser2); 123 | 124 | List actualResponseDTOs = appUserMapperService.entitiesToResponseDtos(appUsers); 125 | 126 | assertEquals(appUsers.size(), actualResponseDTOs.size()); 127 | assertThat(AppUserTestHelper.isEqual(appUsers.get(0), actualResponseDTOs.get(0))).isTrue(); 128 | assertThat(AppUserTestHelper.isEqual(appUsers.get(1), actualResponseDTOs.get(1))).isTrue(); 129 | } 130 | } 131 | 132 | @Nested 133 | class ApplyUpdateDtoToEntity { 134 | 135 | @Test 136 | void entityIsNull_returnNull() { 137 | AppUserUpdateDTO updateDTO = new AppUserUpdateDTO("name", "roleName"); 138 | 139 | AppUser actualAppUser = appUserMapperService.applyUpdateDtoToEntity(null, updateDTO); 140 | 141 | assertNull(actualAppUser); 142 | } 143 | 144 | @Test 145 | void updateDTOIsNull_returnNull() { 146 | AppUser appUser = AppUserTestHelper.getMockAppUser(1, "name", "password"); 147 | 148 | AppUser actualAppUser = appUserMapperService.applyUpdateDtoToEntity(appUser, null); 149 | 150 | assertNull(actualAppUser); 151 | } 152 | 153 | @Test 154 | void roleNotFound_throwFitBuddyException() { 155 | AppUser appUser = AppUserTestHelper.getMockAppUser(1, "name", "password"); 156 | AppUserUpdateDTO updateDTO = new AppUserUpdateDTO("name", "newRoleName"); 157 | 158 | when(roleRepository.findByName(anyString())).thenReturn(Optional.empty()); 159 | 160 | assertThrows(FitBuddyException.class, () -> 161 | appUserMapperService.applyUpdateDtoToEntity(appUser, updateDTO)); 162 | } 163 | 164 | @Test 165 | void returnUpdatedAppUser() { 166 | Role role = RoleTestHelper.getMockRole(1, "roleName"); 167 | AppUser appUser = AppUserTestHelper.getMockAppUser(1, "name", "password", role); 168 | AppUserUpdateDTO updateDTO = new AppUserUpdateDTO("name", "newRoleName"); 169 | 170 | when(roleRepository.findByName(anyString())).thenReturn(Optional.of(role)); 171 | 172 | AppUser actualAppUser = appUserMapperService.applyUpdateDtoToEntity(appUser, updateDTO); 173 | 174 | assertEquals(appUser.getName(), actualAppUser.getName()); 175 | assertEquals(appUser.getRole().getName(), actualAppUser.getRole().getName()); 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/main/resources/static/user/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Home 10 | 11 | 12 |
13 |

FitBuddy

14 |
15 | 16 | 17 |
18 |
19 | 35 |
36 |
37 | 38 | 39 |
40 |
41 |
42 |
43 |
Exercises
44 |
45 |
46 | 47 | 48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 |
#Exercise NameActions
61 |
62 |
63 |
64 |
65 |
66 | 67 | 68 |
69 |
70 |
71 |
72 |
History
73 |
74 |
75 | 76 | 77 | 78 |
79 | 80 |
81 |
82 | 83 | 85 |
86 |
87 | 88 | 89 | 90 | 91 |
92 | 93 |
94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 |
#Exercise NameWeightRepsActions
108 |
109 |
110 |
111 |
112 |
113 | 114 | 115 |
116 |
117 |
118 |
119 |
Account
120 |
121 |
122 |
123 |
124 |
125 | 126 | 127 |
128 |
129 | 130 | 131 |
132 |
133 | 134 | 135 |
136 |
137 | 138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 | 148 |
149 |
150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 50 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 124 | 125 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% ^ 162 | %JVM_CONFIG_MAVEN_PROPS% ^ 163 | %MAVEN_OPTS% ^ 164 | %MAVEN_DEBUG_OPTS% ^ 165 | -classpath %WRAPPER_JAR% ^ 166 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 167 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 168 | if ERRORLEVEL 1 goto error 169 | goto end 170 | 171 | :error 172 | set ERROR_CODE=1 173 | 174 | :end 175 | @endlocal & set ERROR_CODE=%ERROR_CODE% 176 | 177 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 178 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 179 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 180 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 181 | :skipRcPost 182 | 183 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 184 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 185 | 186 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 187 | 188 | cmd /C exit /B %ERROR_CODE% 189 | -------------------------------------------------------------------------------- /src/test/java/app/fitbuddy/service/mapper/HistoryMapperServiceTest.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.service.mapper; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.junit.jupiter.api.Assertions.*; 5 | import static org.mockito.ArgumentMatchers.anyInt; 6 | import static org.mockito.ArgumentMatchers.anyString; 7 | import static org.mockito.Mockito.when; 8 | 9 | import app.fitbuddy.dto.history.HistoryRequestDTO; 10 | import app.fitbuddy.dto.history.HistoryResponseDTO; 11 | import app.fitbuddy.dto.history.HistoryUpdateDTO; 12 | import app.fitbuddy.entity.AppUser; 13 | import app.fitbuddy.entity.Exercise; 14 | import app.fitbuddy.entity.History; 15 | import app.fitbuddy.exception.FitBuddyException; 16 | import app.fitbuddy.testhelper.AppUserTestHelper; 17 | import app.fitbuddy.testhelper.ExerciseTestHelper; 18 | import app.fitbuddy.testhelper.HistoryTestHelper; 19 | 20 | import org.junit.jupiter.api.Nested; 21 | import org.junit.jupiter.api.Test; 22 | import org.junit.jupiter.api.extension.ExtendWith; 23 | import org.mockito.InjectMocks; 24 | import org.mockito.Mock; 25 | import org.mockito.junit.jupiter.MockitoExtension; 26 | 27 | import app.fitbuddy.repository.AppUserRepository; 28 | import app.fitbuddy.repository.ExerciseRepository; 29 | 30 | import java.util.Collections; 31 | import java.util.List; 32 | import java.util.Optional; 33 | 34 | @ExtendWith(MockitoExtension.class) 35 | class HistoryMapperServiceTest { 36 | 37 | @InjectMocks 38 | HistoryMapperService historyMapperService; 39 | 40 | @Mock 41 | AppUserRepository appUserRepository; 42 | 43 | @Mock 44 | ExerciseRepository exerciseRepository; 45 | 46 | @Nested 47 | class RequestDtoToEntity { 48 | 49 | @Test 50 | void requestDTOIsNull_returnNull() { 51 | History actualHistory = historyMapperService.requestDtoToEntity(null); 52 | 53 | assertNull(actualHistory); 54 | } 55 | 56 | @Test 57 | void appUserNotFound_throwFitBuddyException() { 58 | HistoryRequestDTO requestDTO = new HistoryRequestDTO(1, "exerciseName", 11, 111, "2022-01-01"); 59 | 60 | when(appUserRepository.findById(anyInt())).thenReturn(Optional.empty()); 61 | 62 | assertThrows(FitBuddyException.class, () -> historyMapperService.requestDtoToEntity(requestDTO)); 63 | } 64 | 65 | @Test 66 | void exerciseNotFound_throwFitBuddyException() { 67 | HistoryRequestDTO requestDTO = new HistoryRequestDTO(1, "exerciseName", 11, 111, "2022-01-01"); 68 | AppUser appUser = AppUserTestHelper.getMockAppUser(); 69 | 70 | when(appUserRepository.findById(anyInt())).thenReturn(Optional.of(appUser)); 71 | when(exerciseRepository.findByNameAndUserId(anyString(), anyInt())).thenReturn(Optional.empty()); 72 | 73 | assertThrows(FitBuddyException.class, () -> historyMapperService.requestDtoToEntity(requestDTO)); 74 | } 75 | 76 | @Test 77 | void returnHistory() { 78 | HistoryRequestDTO requestDTO = new HistoryRequestDTO(1, "exerciseName", 11, 111, "2022-01-01"); 79 | AppUser appUser = AppUserTestHelper.getMockAppUser(); 80 | Exercise exercise = ExerciseTestHelper.getMockExercise(); 81 | 82 | when(appUserRepository.findById(anyInt())).thenReturn(Optional.of(appUser)); 83 | when(exerciseRepository.findByNameAndUserId(anyString(), anyInt())).thenReturn(Optional.of(exercise)); 84 | 85 | History actualHistory = historyMapperService.requestDtoToEntity(requestDTO); 86 | 87 | assertEquals(requestDTO.getAppUserId(), actualHistory.getAppUser().getId()); 88 | assertEquals(requestDTO.getExerciseName(), actualHistory.getExercise().getName()); 89 | assertEquals(requestDTO.getWeight(), actualHistory.getWeight()); 90 | assertEquals(requestDTO.getReps(), actualHistory.getReps()); 91 | assertEquals(requestDTO.getCreatedOn(), actualHistory.getCreatedOn()); 92 | } 93 | } 94 | 95 | @Nested 96 | class EntityToResponseDto { 97 | 98 | @Test 99 | void entityIsNull_returnNull() { 100 | HistoryResponseDTO actualResponseDTO = historyMapperService.entityToResponseDto(null); 101 | 102 | assertNull(actualResponseDTO); 103 | } 104 | 105 | @Test 106 | void returnResponseDTO() { 107 | Exercise exercise = ExerciseTestHelper.getMockExercise(1, "exerciseName"); 108 | AppUser appUser = AppUserTestHelper.getMockAppUser(11, "name", "password"); 109 | History history = HistoryTestHelper.getMockHistory(111, 22, 222, "2022-01-01", exercise, appUser); 110 | 111 | HistoryResponseDTO actualResponseDTO = historyMapperService.entityToResponseDto(history); 112 | 113 | assertEquals(history.getId(), actualResponseDTO.getId()); 114 | assertEquals(history.getWeight(), actualResponseDTO.getWeight()); 115 | assertEquals(history.getReps(), actualResponseDTO.getReps()); 116 | assertEquals(history.getCreatedOn(), actualResponseDTO.getCreatedOn()); 117 | assertEquals(history.getExercise().getName(), actualResponseDTO.getExerciseName()); 118 | assertEquals(history.getAppUser().getId(), actualResponseDTO.getAppUserId()); 119 | } 120 | } 121 | 122 | @Nested 123 | class EntitiesToResponseDtos { 124 | 125 | @Test 126 | void entitiesIsNull_returnEmptyList() { 127 | List actualResponseDTOs = historyMapperService.entitiesToResponseDtos(null); 128 | 129 | assertEquals(Collections.emptyList(), actualResponseDTOs); 130 | } 131 | 132 | @Test 133 | void entitiesIsEmpty_returnEmptyList() { 134 | List actualResponseDTOs = historyMapperService.entitiesToResponseDtos( 135 | Collections.emptyList()); 136 | 137 | assertEquals(Collections.emptyList(), actualResponseDTOs); 138 | } 139 | 140 | @Test 141 | void returnResponseDTOs() { 142 | Exercise exercise = ExerciseTestHelper.getMockExercise(1, "exerciseName"); 143 | AppUser appUser = AppUserTestHelper.getMockAppUser(11, "name", "password"); 144 | History history1 = HistoryTestHelper.getMockHistory(111, 22, 222, "2022-01-01", exercise, appUser); 145 | History history2 = HistoryTestHelper.getMockHistory(222, 22, 222, "2022-01-01", exercise, appUser); 146 | List histories = List.of(history1, history2); 147 | 148 | List responseDTOs = historyMapperService.entitiesToResponseDtos(histories); 149 | 150 | assertEquals(histories.size(), responseDTOs.size()); 151 | assertThat(HistoryTestHelper.isEqual(histories.get(0), responseDTOs.get(0))).isTrue(); 152 | assertThat(HistoryTestHelper.isEqual(histories.get(1), responseDTOs.get(1))).isTrue(); 153 | } 154 | } 155 | 156 | @Nested 157 | class ApplyUpdateDtoToEntity { 158 | 159 | @Test 160 | void entityIsNull_returnNull() { 161 | HistoryUpdateDTO updateDTO = new HistoryUpdateDTO(1, 2); 162 | 163 | History actualHistory = historyMapperService.applyUpdateDtoToEntity(null, updateDTO); 164 | 165 | assertNull(actualHistory); 166 | } 167 | 168 | @Test 169 | void updateDTOIsNull_returnNull() { 170 | History history = HistoryTestHelper.getMockHistory(1, 1, 2, "2022-01-01"); 171 | 172 | History actualHistory = historyMapperService.applyUpdateDtoToEntity(history, null); 173 | 174 | assertNull(actualHistory); 175 | } 176 | 177 | @Test 178 | void returnUpdatedHistory() { 179 | History history = HistoryTestHelper.getMockHistory(1, 1, 2, "2022-01-01"); 180 | HistoryUpdateDTO updateDTO = new HistoryUpdateDTO(3, 4); 181 | 182 | History actualHistory = historyMapperService.applyUpdateDtoToEntity(history, updateDTO); 183 | 184 | assertEquals(updateDTO.getWeight(), actualHistory.getWeight()); 185 | assertEquals(updateDTO.getReps(), actualHistory.getReps()); 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/main/resources/static/user/js/history.js: -------------------------------------------------------------------------------- 1 | const HISTORY_API_PATH = "/user/history"; 2 | 3 | /* CALENDAR functions */ 4 | 5 | function resetCalendar() { 6 | // Set calendar for the current date 7 | var today = new Date(); 8 | var dd = String(today.getDate()).padStart(2, '0'); 9 | var mm = String(today.getMonth() + 1).padStart(2, '0'); // January is 0! 10 | var yyyy = today.getFullYear(); 11 | today = yyyy + "-" + mm + "-" + dd; 12 | 13 | document.getElementById("calendar").value = today; 14 | } 15 | 16 | function onCalendarChange() { 17 | showHistory(); 18 | } 19 | 20 | function stepUpCalendar() { 21 | document.getElementById("calendar").stepUp(); 22 | onCalendarChange(); 23 | } 24 | 25 | function stepDownCalendar() { 26 | document.getElementById("calendar").stepDown(); 27 | onCalendarChange(); 28 | } 29 | 30 | /* WORKOUT LOG functions */ 31 | 32 | function showHistory() { 33 | refreshExerciseOptions(); 34 | 35 | let date = document.getElementById("calendar").value; 36 | 37 | let xhr = new XMLHttpRequest(); 38 | xhr.open("GET", HISTORY_API_PATH + "/" + date); 39 | xhr.send(); 40 | 41 | xhr.onreadystatechange = function() { 42 | if (this.readyState == XMLHttpRequest.DONE) { 43 | if (this.status == 200) { 44 | // SUCCESS 45 | let data = JSON.parse(this.responseText); 46 | 47 | let tbody = document.getElementById("history-table").getElementsByTagName("tbody")[0]; 48 | 49 | while (tbody.firstChild) { 50 | tbody.removeChild(tbody.firstChild); 51 | } 52 | 53 | let add = ""; 54 | 55 | for (let i = 0; i < data.length; i++) { 56 | let historyId = data[i].id; 57 | let weightId = "history-weight-" + historyId; 58 | let repsId = "history-reps-" + historyId; 59 | let actionsId = "history-actions-" + historyId; 60 | add += "" + (i+1) + "" + 61 | "" + data[i].exerciseName + "" + 62 | "" + data[i].weight + "" + 63 | "" + data[i].reps + "" + 64 | "" + 65 | "" + 66 | "" + 67 | ""; 68 | } 69 | 70 | tbody.innerHTML += add; 71 | hideStatus(); 72 | } else { 73 | // ERROR 74 | console.log("ERROR: " + this.responseText); 75 | } 76 | } 77 | }; 78 | } 79 | 80 | function refreshExerciseOptions() { 81 | let xhr = new XMLHttpRequest(); 82 | xhr.open("GET", EXERCISES_API_PATH); 83 | xhr.send(); 84 | 85 | xhr.onreadystatechange = function() { 86 | if (this.readyState == XMLHttpRequest.DONE) { 87 | if (this.status == 200) { 88 | // SUCCESS 89 | let data = JSON.parse(this.responseText); 90 | 91 | let exerciseSelect = document.getElementById("exercise-select"); 92 | 93 | while(exerciseSelect.firstChild) { 94 | exerciseSelect.removeChild(exerciseSelect.firstChild); 95 | } 96 | 97 | let add = ""; 98 | 99 | for (let i = 0; i < data.length; i++) { 100 | add += ""; 101 | } 102 | 103 | exerciseSelect.innerHTML += add; 104 | } else { 105 | // ERROR 106 | console.log("ERROR: " + this.responseText); 107 | } 108 | } 109 | }; 110 | } 111 | 112 | function deleteHistory(historyId) { 113 | let xhr = new XMLHttpRequest(); 114 | xhr.open("DELETE", HISTORY_API_PATH + "/" + historyId); 115 | xhr.send(); 116 | 117 | xhr.onreadystatechange = function() { 118 | if (this.readyState == XMLHttpRequest.DONE) { 119 | if (this.status == 200) { 120 | // SUCCESS 121 | console.log("OK: " + this.responseText); 122 | } else { 123 | // ERROR 124 | console.log("ERROR: " + this.responseText); 125 | showStatus(this.responseText); 126 | } 127 | showHistory(); 128 | } 129 | } 130 | } 131 | 132 | function onAddHistory() { 133 | let exerciseName = document.getElementById("exercise-select").value; 134 | let weight = document.forms["new-history-form"]["weight"].value; 135 | let reps = document.forms["new-history-form"]["reps"].value; 136 | let createdOn = document.getElementById("calendar").value; 137 | 138 | exerciseName = exerciseName.trim(); 139 | weight = weight.trim(); 140 | reps = reps.trim(); 141 | createdOn = createdOn.trim(); 142 | 143 | let data = { "exerciseName" : exerciseName, 144 | "weight" : weight, 145 | "reps" : reps, 146 | "createdOn" : createdOn }; 147 | 148 | let xhr = new XMLHttpRequest(); 149 | xhr.open("POST", HISTORY_API_PATH); 150 | xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); 151 | xhr.send(JSON.stringify(data)); 152 | 153 | xhr.onreadystatechange = function() { 154 | if (this.readyState == XMLHttpRequest.DONE) { 155 | if (this.status == 200) { 156 | // SUCCESS 157 | console.log("OK: " + this.responseText); 158 | } else { 159 | // ERROR 160 | console.log("ERROR: " + this.responseText); 161 | showStatus(this.responseText); 162 | } 163 | showHistory(); 164 | } 165 | } 166 | 167 | document.forms["new-history-form"]["weight"].value = ""; 168 | document.forms["new-history-form"]["reps"].value = ""; 169 | } 170 | 171 | function editHistory(historyId) { 172 | let weightElement = document.getElementById("history-weight-" + historyId); 173 | let repsElement = document.getElementById("history-reps-" + historyId); 174 | let actionsElement = document.getElementById("history-actions-" + historyId); 175 | // read weight and reps 176 | let weight = weightElement.innerHTML; 177 | let reps = repsElement.innerHTML; 178 | // add input field 179 | weightElement.innerHTML = ""; 180 | repsElement.innerHTML = ""; 181 | // remove edit button 182 | actionsElement.removeChild(actionsElement.firstChild); 183 | // add save button 184 | actionsElement.innerHTML = "" + 185 | actionsElement.innerHTML; 186 | } 187 | 188 | function saveHistory(historyId) { 189 | // read fields 190 | let weightElement = document.getElementById("history-weight-" + historyId); 191 | let repsElement = document.getElementById("history-reps-" + historyId); 192 | let weight = weightElement.firstChild.value; 193 | let reps = repsElement.firstChild.value; 194 | // get date 195 | let createdOn = document.getElementById("calendar").value; 196 | 197 | weight = weight.trim(); 198 | reps = reps.trim(); 199 | createdOn = createdOn.trim(); 200 | 201 | let data = { "weight" : weight, 202 | "reps" : reps, 203 | "createdOn" : createdOn }; 204 | 205 | let xhr = new XMLHttpRequest(); 206 | xhr.open("PUT", HISTORY_API_PATH + "/" + historyId); 207 | xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); 208 | xhr.send(JSON.stringify(data)); 209 | 210 | xhr.onreadystatechange = function() { 211 | if (this.readyState == XMLHttpRequest.DONE) { 212 | if (this.status == 200) { 213 | // SUCCESS 214 | console.log("OK: " + this.responseText); 215 | } else { 216 | // ERROR 217 | console.log("ERROR: " + this.responseText); 218 | showStatus(this.responseText); 219 | } 220 | showHistory(); 221 | } 222 | } 223 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

FitBuddy

2 | 3 |
4 | 5 | 6 | [![All Contributors](https://img.shields.io/badge/all_contributors-13-orange.svg?style=flat-square)](#contributors-) 7 | 8 | 9 | [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=fitbuddy-app&metric=sqale_rating)](https://sonarcloud.io/summary/overall?id=fitbuddy-app) 10 | [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=fitbuddy-app&metric=reliability_rating)](https://sonarcloud.io/summary/overall?id=fitbuddy-app) 11 | [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=fitbuddy-app&metric=security_rating)](https://sonarcloud.io/summary/overall?id=fitbuddy-app) 12 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=fitbuddy-app&metric=alert_status)](https://sonarcloud.io/summary/overall?id=fitbuddy-app) 13 | 14 | [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=fitbuddy-app&metric=ncloc)](https://sonarcloud.io/summary/overall?id=fitbuddy-app) 15 | [![Duplicated Lines (%)](https://sonarcloud.io/api/project_badges/measure?project=fitbuddy-app&metric=duplicated_lines_density)](https://sonarcloud.io/summary/overall?id=fitbuddy-app) 16 | [![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=fitbuddy-app&metric=code_smells)](https://sonarcloud.io/summary/overall?id=fitbuddy-app) 17 | [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=fitbuddy-app&metric=coverage)](https://sonarcloud.io/summary/overall?id=fitbuddy-app) 18 | 19 | ![Build](https://github.com/mepox/fitbuddy/actions/workflows/build.yml/badge.svg) 20 | 21 |
22 | 23 | FitBuddy is a workout tracker app made with Spring Boot. 24 | 25 | ## What's inside 26 | 27 | The project uses the following technologies: 28 | 29 | Backend: 30 | - Java 11 31 | - Spring Boot with Spring Security and Spring Data JPA 32 | - RESTful API 33 | - Lombok 34 | - SQL 35 | - Maven 36 | 37 | Frontend: 38 | - Bootstrap 5 with Bootstrap Icons 39 | - HTML, CSS and JavaScript 40 | 41 | ## Features 42 | 43 | - Users can register and login to the application. 44 | - Users can create their custom exercises. 45 | - Users can log their workout by adding their own custom exercises to a specific date. 46 | 47 | ## Installation 48 | 49 | The project is created with Maven, so you just need to import it to your IDE and build the project to resolve the dependencies. 50 | 51 | Maven Wrapper included in the project, you can start the app with: `mvnw spring-boot:run` without installing Maven. 52 | 53 | Alternatively, you can start the project with Maven: `mvn spring-boot:run` 54 | 55 | ## Running the application 56 | 57 | A default user with some preloaded data is added at the start of the application. 58 | 59 | ``` 60 | username: user 61 | password: user 62 | ``` 63 | 64 | ## Contributing 65 | 66 | Please view the [Contributing guidelines](https://github.com/mepox/fitbuddy/blob/main/CONTRIBUTING.md) how can you contribute to this project. 67 | 68 | ## Live demo 69 | 70 | https://fitbuddy-demo.up.railway.app/ 71 | 72 | ## Screenshots 73 | 74 |
75 | 76 | 77 | 78 | 79 | 80 | 81 |
82 | 83 | ## Contributors 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 |
mepox
mepox

💻
Haseeb Ansari
Haseeb Ansari

💻
isKaled
isKaled

💻
Ujwal Kumar
Ujwal Kumar

💻
Keerthi Sai Maganti
Keerthi Sai Maganti

💻
SwethaTamatam
SwethaTamatam

💻
dmitriydb
dmitriydb

💻
Gaurav_mango
Gaurav_mango

💻
Harmanjit
Harmanjit

💻
Maximo Vincente Mejia
Maximo Vincente Mejia

💻
Mathew Torres
Mathew Torres

💻
Ravi Mandalia
Ravi Mandalia

💻
cerrussell
cerrussell

💻
109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /src/test/java/app/fitbuddy/controller/crud/ExerciseControllerTest.java: -------------------------------------------------------------------------------- 1 | package app.fitbuddy.controller.crud; 2 | 3 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 4 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 5 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; 6 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; 7 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 8 | 9 | import java.util.List; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | import static org.mockito.ArgumentMatchers.any; 13 | import static org.mockito.ArgumentMatchers.anyInt; 14 | import static org.mockito.Mockito.times; 15 | import static org.mockito.Mockito.verify; 16 | import static org.mockito.Mockito.when; 17 | 18 | import org.junit.jupiter.api.Nested; 19 | import org.junit.jupiter.api.Test; 20 | import org.junit.jupiter.params.ParameterizedTest; 21 | import org.junit.jupiter.params.provider.ValueSource; 22 | import org.springframework.beans.factory.annotation.Autowired; 23 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 24 | import org.springframework.boot.test.mock.mockito.MockBean; 25 | import org.springframework.http.MediaType; 26 | import org.springframework.security.test.context.support.WithAnonymousUser; 27 | import org.springframework.test.context.ContextConfiguration; 28 | import org.springframework.test.web.servlet.MockMvc; 29 | import org.springframework.test.web.servlet.MvcResult; 30 | 31 | import com.fasterxml.jackson.core.type.TypeReference; 32 | import com.fasterxml.jackson.databind.ObjectMapper; 33 | 34 | import app.fitbuddy.FitBuddyApplication; 35 | import app.fitbuddy.config.SecurityConfig; 36 | import app.fitbuddy.dto.exercise.ExerciseRequestDTO; 37 | import app.fitbuddy.dto.exercise.ExerciseResponseDTO; 38 | import app.fitbuddy.dto.exercise.ExerciseUpdateDTO; 39 | import app.fitbuddy.service.crud.ExerciseCrudService; 40 | import app.fitbuddy.testhelper.annotation.WithMockAppUserPrincipal; 41 | 42 | @WebMvcTest(ExerciseController.class) 43 | @ContextConfiguration(classes = {FitBuddyApplication.class, SecurityConfig.class}) 44 | class ExerciseControllerTest { 45 | 46 | @Autowired 47 | MockMvc mockMvc; 48 | 49 | @Autowired 50 | ObjectMapper objectMapper; 51 | 52 | @MockBean 53 | ExerciseCrudService exerciseCrudService; 54 | 55 | final String API_PATH = "/user/exercises"; 56 | 57 | @Nested 58 | @WithMockAppUserPrincipal(authority = "USER") 59 | class Create { 60 | 61 | @Test 62 | @WithAnonymousUser 63 | void whenNotAuthed_shouldReturnRedirect302() throws Exception { 64 | ExerciseRequestDTO requestDTO = new ExerciseRequestDTO("exerciseName", null); 65 | 66 | mockMvc.perform(post(API_PATH) 67 | .contentType(MediaType.APPLICATION_JSON) 68 | .content(objectMapper.writeValueAsString(requestDTO))) 69 | .andExpect(status().is(302)); 70 | } 71 | 72 | @ParameterizedTest 73 | @ValueSource(strings = {"", "exerciseNameexerciseNameexerciseName"}) // <1 or >32 74 | void whenExerciseNameSizeNotCorrect_shouldReturnBadRequest(String name) throws Exception { 75 | ExerciseRequestDTO requestDTO = new ExerciseRequestDTO(name, null); 76 | 77 | mockMvc.perform(post(API_PATH) 78 | .contentType(MediaType.APPLICATION_JSON) 79 | .content(objectMapper.writeValueAsString(requestDTO))) 80 | .andExpect(status().isBadRequest()); 81 | } 82 | 83 | @Test 84 | void whenInputIsCorrect_shouldReturnOk() throws Exception { 85 | ExerciseRequestDTO requestDTO = new ExerciseRequestDTO("exerciseName", null); 86 | 87 | mockMvc.perform(post(API_PATH) 88 | .contentType(MediaType.APPLICATION_JSON) 89 | .content(objectMapper.writeValueAsString(requestDTO))) 90 | .andExpect(status().isOk()); 91 | 92 | verify(exerciseCrudService).create(any(ExerciseRequestDTO.class)); 93 | } 94 | } 95 | 96 | @Nested 97 | @WithMockAppUserPrincipal(authority = "USER") 98 | class ReadAll { 99 | 100 | @Test 101 | @WithAnonymousUser 102 | void whenNotAuthed_shouldReturnRedirect302() throws Exception { 103 | mockMvc.perform(get(API_PATH)).andExpect(status().is(302)); 104 | } 105 | 106 | @Test 107 | void whenAuthed_shouldReturnExerciseDtoList() throws Exception { 108 | ExerciseResponseDTO exerciseResponseDTO_1 = new ExerciseResponseDTO(1, "exerciseName", 11); 109 | ExerciseResponseDTO exerciseResponseDTO_2 = new ExerciseResponseDTO(2, "exerciseName", 11); 110 | List exerciseResponseDTOs = List.of(exerciseResponseDTO_1, exerciseResponseDTO_2); 111 | 112 | when(exerciseCrudService.readMany(anyInt())).thenReturn(exerciseResponseDTOs); 113 | 114 | MvcResult mvcResult = mockMvc.perform(get(API_PATH)).andExpect(status().isOk()).andReturn(); 115 | 116 | List actualExerciseResponseDTOs = objectMapper.readValue( 117 | mvcResult.getResponse().getContentAsString(), 118 | new TypeReference>() {}); 119 | 120 | assertEquals(exerciseResponseDTOs.size(), actualExerciseResponseDTOs.size()); 121 | assertEquals(exerciseResponseDTOs.get(0), actualExerciseResponseDTOs.get(0)); 122 | assertEquals(exerciseResponseDTOs.get(1), actualExerciseResponseDTOs.get(1)); 123 | } 124 | } 125 | 126 | @Nested 127 | @WithMockAppUserPrincipal(authority = "USER") 128 | class Update { 129 | 130 | @Test 131 | @WithAnonymousUser 132 | void whenNotAuthed_shouldReturnRedirect302() throws Exception { 133 | mockMvc.perform(put(API_PATH)).andExpect(status().is(302)); 134 | } 135 | 136 | @Test 137 | void whenPathVariableNotInteger_shouldReturnBadRequest() throws Exception { 138 | ExerciseUpdateDTO udpateDTO = new ExerciseUpdateDTO("exerciseName"); 139 | 140 | mockMvc.perform(put(API_PATH + "/abc") 141 | .contentType(MediaType.APPLICATION_JSON) 142 | .content(objectMapper.writeValueAsString(udpateDTO))) 143 | .andExpect(status().isBadRequest()); 144 | } 145 | 146 | @ParameterizedTest 147 | @ValueSource(strings = {"", "exerciseNameexerciseNameexerciseName"}) // <1 or >32 148 | void whenExerciseNameSizeNotCorrect_shouldReturnBadRequest(String name) throws Exception { 149 | ExerciseUpdateDTO udpateDTO = new ExerciseUpdateDTO(name); 150 | 151 | mockMvc.perform(put(API_PATH + "/1") 152 | .contentType(MediaType.APPLICATION_JSON) 153 | .content(objectMapper.writeValueAsString(udpateDTO))) 154 | .andExpect(status().isBadRequest()); 155 | } 156 | 157 | @Test 158 | void whenInputIsCorrect_shouldReturnOk() throws Exception { 159 | ExerciseResponseDTO responseDTO = new ExerciseResponseDTO(1, "oldExerciseName", 1); 160 | ExerciseUpdateDTO udpateDTO = new ExerciseUpdateDTO("exerciseName"); 161 | 162 | when(exerciseCrudService.readById(anyInt())).thenReturn(responseDTO); 163 | 164 | mockMvc.perform(put(API_PATH + "/1") 165 | .contentType(MediaType.APPLICATION_JSON) 166 | .content(objectMapper.writeValueAsString(udpateDTO))) 167 | .andExpect(status().isOk()); 168 | 169 | verify(exerciseCrudService).update(1, udpateDTO); 170 | } 171 | } 172 | 173 | @Nested 174 | @WithMockAppUserPrincipal(authority = "USER") 175 | class Delete { 176 | 177 | @Test 178 | @WithAnonymousUser 179 | void whenNotAuthed_shouldReturnRedirect302() throws Exception { 180 | mockMvc.perform(delete(API_PATH)).andExpect(status().is(302)); 181 | } 182 | 183 | @Test 184 | void whenPathVariableNotInteger_shouldReturnBadRequest() throws Exception { 185 | mockMvc.perform(delete(API_PATH + "/abc")).andExpect(status().isBadRequest()); 186 | } 187 | 188 | @Test 189 | void whenAppUserIdDoesntMatch_shouldReturnBadRequest() throws Exception { 190 | ExerciseResponseDTO exerciseResponseDTO = new ExerciseResponseDTO(1, "exerciseName", 22); 191 | 192 | when(exerciseCrudService.readById(anyInt())).thenReturn(exerciseResponseDTO); 193 | 194 | mockMvc.perform(delete(API_PATH + "/1")).andExpect(status().isBadRequest()); 195 | 196 | verify(exerciseCrudService, times(0)).delete(anyInt()); 197 | } 198 | 199 | @Test 200 | void whenInputIsCorrect_shouldReturnOk() throws Exception { 201 | ExerciseResponseDTO exerciseResponseDTO = new ExerciseResponseDTO(1, "exerciseName", 1); 202 | 203 | when(exerciseCrudService.readById(anyInt())).thenReturn(exerciseResponseDTO); 204 | 205 | mockMvc.perform(delete(API_PATH + "/1")).andExpect(status().isOk()); 206 | 207 | verify(exerciseCrudService).delete(1); 208 | } 209 | } 210 | } 211 | --------------------------------------------------------------------------------