Repository: tomerpeceniski/igym
Pull request #18: Feat/workout create route
Author: login: danssaul
Created: 2025-04-17T17:20:59Z | Merged: 2025-04-21T13:57:41Z

# Workout create route

  This route allows user to create a workout containing a list of exercises with all validations of these entities. I have added javadoc to the code and a new github actions workflow that:
 1. Run mvn javadoc:javadoc on push to main (or on demand)
 2. Copy the generated HTML from target/site/apidocs
 3. Push it to a gh-pages branch
 4. Enable GitHub Pages to serve it as a site like: https://<your-username>.github.io/<your-repo>/

The workflow automatically builds the documentation and publishes it to GitHub Pages from the gh-pages branch.
Can you please enable GitHub Pages in the repo settings with the following configuration @tomerpeceniski ?

"Source: Deploy from a branch"
"Branch: gh-pages"
"Branch: gh-pages"

This will let us access the Javadoc online at:
https://<your-username>.github.io/igym/

Let me know if you need anything from my side!

## Checklist
- [x] All acceptance criteria have been met
- [x] My code follows the project's coding style.
- [x] I have documented my code
- [x] I wrote unit tests for the changes that I made

## Task in Notion
[task on notion](https://www.notion.so/16bb8830dc7b8068807af79065c08418?v=16bb8830dc7b801292da000c63c754b1&p=1ceb8830dc7b80258a32ec76936ea3cf&pm=s)



Diff:
diff --git a/.github/workflows/javadoc.yml b/.github/workflows/javadoc.yml
new file mode 100644
index 00000000..269e0fdd
--- /dev/null
+++ b/.github/workflows/javadoc.yml
@@ -0,0 +1,34 @@
+name: Deploy Javadoc to GitHub Pages
+
+on:
+  push:
+    branches:
+      - main
+      - feat/*
+
+jobs:
+  deploy:
+    runs-on: ubuntu-latest
+
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v3
+
+    - name: Set up Java 21
+      uses: actions/setup-java@v3
+      with:
+        java-version: '21'
+        distribution: 'temurin'
+
+    - name: Build Javadoc
+      run: mvn clean javadoc:javadoc
+  
+    - name: List contents of generated site
+      run: ls -R ./target/site/apidocs
+  
+    - name: Deploy to GitHub Pages
+      uses: peaceiris/actions-gh-pages@v3
+      with:
+        github_token: ${{ secrets.GITHUB_TOKEN }}
+        publish_branch: gh-pages
+        publish_dir: ./target/site/apidocs
diff --git a/pom.xml b/pom.xml
index 453b889e..a75d180c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -83,13 +83,20 @@
 			<groupId>org.springframework.boot</groupId>
 			<artifactId>spring-boot-starter-validation</artifactId>
 		</dependency>
-
+		<dependency>
+			<groupId>org.junit.jupiter</groupId>
+			<artifactId>junit-jupiter-params</artifactId>
+			<version>5.11.4</version>
+			<scope>test</scope>
+		</dependency>
 		<dependency>
 			<groupId>org.projectlombok</groupId>
 			<artifactId>lombok</artifactId>
-			<version>1.18.30</version>
+			<version>1.18.30</version> <!-- Check for latest version if needed -->
 			<scope>provided</scope>
 		</dependency>
+
+
 	</dependencies>
 
 	<build>
@@ -122,6 +129,19 @@
 					</execution>
 				</executions>
 			</plugin>
+			<plugin>
+				<groupId>org.apache.maven.plugins</groupId>
+				<artifactId>maven-javadoc-plugin</artifactId>
+				<version>3.10.1</version>
+				<configuration>
+					<failOnError>false</failOnError>
+					<doclint>none</doclint>
+					<encoding>UTF-8</encoding>
+					<source>21</source>
+					<outputDirectory>${project.build.directory}/site</outputDirectory>
+					<additionalJOption>-Xdoclint:none</additionalJOption>
+				</configuration>
+			</plugin>
 		</plugins>
 	</build>
 
diff --git a/src/main/java/igym/controllers/WorkoutController.java b/src/main/java/igym/controllers/WorkoutController.java
new file mode 100644
index 00000000..f6d61f88
--- /dev/null
+++ b/src/main/java/igym/controllers/WorkoutController.java
@@ -0,0 +1,41 @@
+package igym.controllers;
+
+import igym.entities.Workout;
+import igym.services.WorkoutService;
+import jakarta.validation.Valid;
+
+import java.util.UUID;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * REST controller for managing workout creation.
+ */
+@RequestMapping(value = "/api")
+@RestController
+public class WorkoutController {
+
+    private final WorkoutService workoutService;
+
+    public WorkoutController(WorkoutService workoutService) {
+        this.workoutService = workoutService;
+    }
+
+    /**
+     * Creates a new workout via POST request.
+     * 
+     * @param workout the workout to create
+     * @return the created workout and HTTP 201 status
+     */
+
+    @PostMapping(value = "/gyms/{gymId}/workouts", produces = "application/json")
+    public ResponseEntity<Workout> createWorkout(
+            @PathVariable UUID gymId,
+            @RequestBody @Valid Workout workout) {
+        Workout createdWorkout = workoutService.createWorkout(workout, gymId);
+        return ResponseEntity.status(HttpStatus.CREATED).body(createdWorkout);
+    }
+
+}
diff --git a/src/main/java/igym/entities/Exercise.java b/src/main/java/igym/entities/Exercise.java
new file mode 100644
index 00000000..5a192ec0
--- /dev/null
+++ b/src/main/java/igym/entities/Exercise.java
@@ -0,0 +1,53 @@
+package igym.entities;
+
+import jakarta.persistence.*;
+import jakarta.validation.constraints.*;
+import lombok.*;
+
+import java.util.UUID;
+
+import com.fasterxml.jackson.annotation.JsonBackReference;
+
+/**
+ * Represents an exercise within a workout.
+ *
+ * Lombok is used to generate getters and setters for convenience.
+ * The {@code id} field is annotated with {@code @Setter(AccessLevel.NONE)}
+ * to avoid manual assignment, as it is automatically generated by the database.
+ *
+ * Field-level validations ensure input correctness and consistency across
+ * layers.
+ */
+
+@Entity
+@Getter
+@Setter
+@NoArgsConstructor
+public class Exercise {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.UUID)
+    @Setter(AccessLevel.NONE)
+    private UUID id;
+
+    @NotBlank(message = "Exercise name is required")
+    @Column(nullable = false, length = 50)
+    @Size(min = 3, max = 50, message = "Name must be between 3 and 50 characters")
+    private String name;
+
+    @PositiveOrZero(message = "Weight must be zero or positive")
+    private double weight;
+
+    @Min(value = 1, message = "Reps must be at least 1")
+    private int numReps;
+
+    @Min(value = 1, message = "Sets must be at least 1")
+    private int numSets;
+
+    private String note;
+
+    @ManyToOne
+    @JoinColumn(name = "workout_id", nullable = false)
+    @JsonBackReference
+    private Workout workout;
+}
diff --git a/src/main/java/igym/entities/Gym.java b/src/main/java/igym/entities/Gym.java
index 184e52b2..c81b31fb 100644
--- a/src/main/java/igym/entities/Gym.java
+++ b/src/main/java/igym/entities/Gym.java
@@ -1,10 +1,13 @@
 package igym.entities;
 
 import java.time.Instant;
+import java.util.List;
 import java.util.UUID;
 
 import org.hibernate.annotations.UpdateTimestamp;
 
+import com.fasterxml.jackson.annotation.JsonManagedReference;
+
 import igym.entities.enums.Status;
 import jakarta.persistence.*;
 import jakarta.validation.constraints.*;
@@ -31,6 +34,10 @@ public class Gym {
     @UpdateTimestamp
     private Instant updated_at;
 
+    @OneToMany(mappedBy = "gym", cascade = CascadeType.ALL)
+    @JsonManagedReference
+    private List<Workout> workouts;
+
     public Gym(String name) {
         this.name = name;
     }
diff --git a/src/main/java/igym/entities/Workout.java b/src/main/java/igym/entities/Workout.java
new file mode 100644
index 00000000..315dbfa8
--- /dev/null
+++ b/src/main/java/igym/entities/Workout.java
@@ -0,0 +1,46 @@
+package igym.entities;
+
+import jakarta.persistence.*;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.*;
+import lombok.*;
+
+import java.util.List;
+import java.util.UUID;
+
+import com.fasterxml.jackson.annotation.JsonBackReference;
+import com.fasterxml.jackson.annotation.JsonManagedReference;
+
+/**
+ * Represents a workout plan which contains multiple exercises.
+ * Lombok's @Setter is applied to the class, but the ID field
+ * uses @Setter(AccessLevel.NONE)
+ * to prevent accidental assignment, as it's auto-generated.
+ */
+@Entity
+@Getter
+@Setter
+@NoArgsConstructor
+public class Workout {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.UUID)
+    @Setter(AccessLevel.NONE)
+    private UUID id;
+
+    @NotBlank(message = "Workout name is required")
+    @Column(nullable = false, length = 50)
+    @Size(min = 1, max = 50, message = "Name must be between 3 and 50 characters")
+    private String name;
+
+    @NotEmpty(message = "Workout must contain at least one exercise")
+    @OneToMany(mappedBy = "workout", cascade = CascadeType.ALL)
+    @Valid
+    @JsonManagedReference
+    private List<Exercise> exerciseList;
+
+    @ManyToOne(optional = false)
+    @JoinColumn(name = "gym_id", nullable = false)
+    @JsonBackReference
+    private Gym gym;
+}
diff --git a/src/main/java/igym/exceptions/GlobalExceptionHandler.java b/src/main/java/igym/exceptions/GlobalExceptionHandler.java
index 564f3e49..2fec4879 100644
--- a/src/main/java/igym/exceptions/GlobalExceptionHandler.java
+++ b/src/main/java/igym/exceptions/GlobalExceptionHandler.java
@@ -29,15 +29,14 @@ private Map<String, Object> buildResponseBody(HttpStatus status, String message)
 
     @ExceptionHandler(MethodArgumentNotValidException.class)
     public ResponseEntity<Object> handleValidationException(MethodArgumentNotValidException ex) {
-        logger.error(ex.getMessage());
-        Map<String, Object> body = buildResponseBody(HttpStatus.BAD_REQUEST, HttpStatus.BAD_REQUEST.getReasonPhrase());
+        Map<String, Object> body = buildResponseBody(HttpStatus.UNPROCESSABLE_ENTITY, HttpStatus.UNPROCESSABLE_ENTITY.getReasonPhrase());
 
         List<String> errors = ex.getBindingResult().getFieldErrors().stream().map(e -> e.getDefaultMessage()).toList();
 
         body.put("message", "Validation error");
         body.put("errors", errors);
 
-        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body);
+        return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(body);
     }
 
     @ExceptionHandler(DuplicateGymException.class)
diff --git a/src/main/java/igym/repositories/WorkoutRepository.java b/src/main/java/igym/repositories/WorkoutRepository.java
new file mode 100644
index 00000000..378d4ae2
--- /dev/null
+++ b/src/main/java/igym/repositories/WorkoutRepository.java
@@ -0,0 +1,10 @@
+package igym.repositories;
+
+import java.util.UUID;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import igym.entities.Workout;
+
+public interface WorkoutRepository extends JpaRepository<Workout, UUID> {
+}
\ No newline at end of file
diff --git a/src/main/java/igym/services/GymService.java b/src/main/java/igym/services/GymService.java
index 8b1ecaf8..5abc8180 100644
--- a/src/main/java/igym/services/GymService.java
+++ b/src/main/java/igym/services/GymService.java
@@ -39,15 +39,22 @@ public Gym createGym(Gym gym) {
 
     public Gym updateGym(UUID id, String name) {
         logger.info("Attempting to update Gym with id: {}", id);
-        Gym gym = gymRepository.findById(id)
-                .orElseThrow(() -> new GymNotFoundException("Gym with id " + id + " not found."));
+
+        Gym gym = findById(id);
         if (gymRepository.existsByName(name)) {
             throw new DuplicateGymException("A gym with the name '" + name + "' already exists.");
         }
+        if (gym.getStatus() == Status.inactive) {
+            logger.warn("Gym with id {} is inactive", id);
+            throw new GymNotFoundException("Gym with id " + id + " not found");
+        }
+
         gym.setName(name);
         Gym savedGym = gymRepository.save(gym);
+        
         logger.info("Gym with id {} updated sucessfully", id);
         logger.debug("Updated Gym persisted: {}", savedGym);
+
         return savedGym;
     }
 
@@ -61,15 +68,30 @@ public List<Gym> findAllGyms() {
 
     public void deleteGym(UUID id) {
         logger.info("Attempting to inactivate gym with id {}", id);
-        Gym gym = (gymRepository.findById(id))
-                .orElseThrow(() -> new GymNotFoundException("Gym with id " + id + " not found"));
+        Gym gym = findById(id);
 
-        if (gym.getStatus() == Status.inactive)
+        if (gym.getStatus() == Status.inactive) {
+            logger.warn("Gym with id {} is inactive", id);
             throw new GymNotFoundException("Gym with id " + id + " not found");
+        }
 
         gym.setStatus(Status.inactive);
         gymRepository.save(gym);
         logger.info("Gym with id {} inactivated", id);
     }
-    
+
+    public Gym findById(UUID id) {
+        logger.info("Fetching gym with id: {}", id);
+
+        Gym gym = gymRepository.findById(id)
+                .orElseThrow(() -> new GymNotFoundException("Gym with id " + id + " not found"));
+        if (gym.getStatus() == Status.inactive) {
+            logger.warn("Gym with id {} is inactive", id);
+            throw new GymNotFoundException("Gym with id " + id + " not found");
+        }
+
+        logger.debug("Fetched gym: {}", gym);
+        return gym;
+    }
+
 }
\ No newline at end of file
diff --git a/src/main/java/igym/services/WorkoutService.java b/src/main/java/igym/services/WorkoutService.java
new file mode 100644
index 00000000..dc5cb741
--- /dev/null
+++ b/src/main/java/igym/services/WorkoutService.java
@@ -0,0 +1,43 @@
+package igym.services;
+
+import igym.entities.Exercise;
+import igym.entities.Gym;
+import igym.entities.Workout;
+import igym.repositories.WorkoutRepository;
+import jakarta.transaction.Transactional;
+
+import java.util.UUID;
+
+import org.springframework.stereotype.Service;
+
+@Service
+public class WorkoutService {
+
+    private final WorkoutRepository workoutRepository;
+    private final GymService gymService;
+
+    public WorkoutService(WorkoutRepository workoutRepository, GymService gymService) {
+        this.workoutRepository = workoutRepository;
+        this.gymService = gymService;
+    }
+
+    /**
+     * Creates a new workout with its list of exercises.
+     * 
+     * @param workout the workout entity to save
+     * @return the saved workout
+     */
+    @Transactional
+    public Workout createWorkout(Workout workout, UUID gymId) {
+        Gym gym = gymService.findById(gymId);
+        workout.setGym(gym);
+
+        if (workout.getExerciseList() != null) {
+            for (Exercise exercise : workout.getExerciseList()) {
+                exercise.setWorkout(workout);
+            }
+        }
+
+        return workoutRepository.save(workout);
+    }
+}
diff --git a/src/test/java/igym/controllers/GymControllerTest.java b/src/test/java/igym/controllers/GymControllerTest.java
index 7cb16bbf..567d2a74 100644
--- a/src/test/java/igym/controllers/GymControllerTest.java
+++ b/src/test/java/igym/controllers/GymControllerTest.java
@@ -94,12 +94,12 @@ void testCreateGymWithDuplicateName() throws Exception {
         }
 
         @Test
-        @DisplayName("should return 400 Bad Request when name is null")
+        @DisplayName("should return 422 Unprocessable Entity when name is null")
         void testCreateGymWithNullName() throws Exception {
                 mockMvc.perform(post("/api/gyms")
                                 .contentType(MediaType.APPLICATION_JSON)
                                 .content(objectMapper.writeValueAsString(new Gym(null))))
-                                .andExpect(status().isBadRequest())
+                                .andExpect(status().isUnprocessableEntity())
                                 .andExpect(jsonPath("$.message").value("Validation error"))
                                 .andExpect(jsonPath("$.errors").value("Name cannot be blank"));
 
@@ -107,12 +107,12 @@ void testCreateGymWithNullName() throws Exception {
         }
 
         @Test
-        @DisplayName("should return 400 Bad Request when name an empty string")
+        @DisplayName("should return 422 Unprocessable Entity when name an empty string")
         void testCreateGymWithEmptyStringName() throws Exception {
                 mockMvc.perform(post("/api/gyms")
                                 .contentType(MediaType.APPLICATION_JSON)
                                 .content(objectMapper.writeValueAsString(new Gym(""))))
-                                .andExpect(status().isBadRequest())
+                                .andExpect(status().isUnprocessableEntity())
                                 .andExpect(jsonPath("$.errors", hasSize(2))) // Ensure two errors exist
                                 .andExpect(jsonPath("$.errors", containsInAnyOrder(
                                                 "Name cannot be blank",
@@ -122,12 +122,12 @@ void testCreateGymWithEmptyStringName() throws Exception {
         }
 
         @Test
-        @DisplayName("should return 400 Bad Request when name has more than 50 characters")
+        @DisplayName("should return 422 Unprocessable Entity when name has more than 50 characters")
         void testCreateGymWithTooLongName() throws Exception {
                 mockMvc.perform(post("/api/gyms")
                                 .contentType(MediaType.APPLICATION_JSON)
                                 .content(objectMapper.writeValueAsString(new Gym("a".repeat(51)))))
-                                .andExpect(status().isBadRequest())
+                                .andExpect(status().isUnprocessableEntity())
                                 .andExpect(jsonPath("$.message").value("Validation error"))
                                 .andExpect(jsonPath("$.errors").value("Name must be between 3 and 50 characters"));
 
@@ -135,12 +135,12 @@ void testCreateGymWithTooLongName() throws Exception {
         }
 
         @Test
-        @DisplayName("should return 400 Bad Request when name has less than 3 characters")
+        @DisplayName("should return 422 Unprocessable Entity when name has less than 3 characters")
         void testCreateGymWithTooShortName() throws Exception {
                 mockMvc.perform(post("/api/gyms")
                                 .contentType(MediaType.APPLICATION_JSON)
                                 .content(objectMapper.writeValueAsString(new Gym("a"))))
-                                .andExpect(status().isBadRequest())
+                                .andExpect(status().isUnprocessableEntity())
                                 .andExpect(jsonPath("$.message").value("Validation error"))
                                 .andExpect(jsonPath("$.errors").value("Name must be between 3 and 50 characters"));
 
@@ -247,14 +247,14 @@ void testUpdateGymNameAlreadyInUse() throws Exception {
         }
 
         @Test
-        @DisplayName("should return 400 when trying to update gym with invalid name")
+        @DisplayName("should return 422 when trying to update gym with invalid name")
         void testUpdateGymInvalidName() throws Exception {
                 Gym invalidGym = new Gym("");
 
                 mockMvc.perform(patch("/api/gyms/{gymId}", gymId)
                                 .contentType(MediaType.APPLICATION_JSON)
                                 .content(objectMapper.writeValueAsString(invalidGym)))
-                                .andExpect(status().isBadRequest())
+                                .andExpect(status().isUnprocessableEntity())
                                 .andExpect(jsonPath("$.message").value("Validation error"))
                                 .andExpect(jsonPath("$.errors", hasSize(2)))
                                 .andExpect(jsonPath("$.errors", containsInAnyOrder(
diff --git a/src/test/java/igym/controllers/UserControllerTest.java b/src/test/java/igym/controllers/UserControllerTest.java
index 20f2d1b3..14d2d030 100644
--- a/src/test/java/igym/controllers/UserControllerTest.java
+++ b/src/test/java/igym/controllers/UserControllerTest.java
@@ -133,12 +133,12 @@ void createUserTest() throws Exception {
         }
 
         @Test
-        @DisplayName("Should return status 400 when creating user with null name")
+        @DisplayName("Should return status 422 Unprocessable Entity when creating user with null name")
         void createNullNameUserTest() throws Exception {
                 mockMvc.perform(post("/users")
                                 .contentType(MediaType.APPLICATION_JSON)
                                 .content(objectMapper.writeValueAsString(new User())))
-                                .andExpect(status().isBadRequest())
+                                .andExpect(status().isUnprocessableEntity())
                                 .andExpect(jsonPath("$.message").value("Validation error"))
                                 .andExpect(jsonPath("$.errors").value("Name cannot be blank"));
 
@@ -146,12 +146,12 @@ void createNullNameUserTest() throws Exception {
         }
 
         @Test
-        @DisplayName("Should return status 400 when creating user with blank name")
+        @DisplayName("Should return status 422 Unprocessable Entity when creating user with blank name")
         void createBlankNameUserTest() throws Exception {
                 mockMvc.perform(post("/users")
                                 .contentType(MediaType.APPLICATION_JSON)
                                 .content(objectMapper.writeValueAsString(new User("     "))))
-                                .andExpect(status().isBadRequest())
+                                .andExpect(status().isUnprocessableEntity())
                                 .andExpect(jsonPath("$.message").value("Validation error"))
                                 .andExpect(jsonPath("$.errors").value("Name cannot be blank"));
 
@@ -159,12 +159,12 @@ void createBlankNameUserTest() throws Exception {
         }
 
         @Test
-        @DisplayName("Should return status 400 when creating user with empty name")
+        @DisplayName("Should return status 422 Unprocessable Entity when creating user with empty name")
         void createEmptyNameUserTest() throws Exception {
                 mockMvc.perform(post("/users")
                                 .contentType(MediaType.APPLICATION_JSON)
                                 .content(objectMapper.writeValueAsString(new User(""))))
-                                .andExpect(status().isBadRequest())
+                                .andExpect(status().isUnprocessableEntity())
                                 .andExpect(jsonPath("$.errors", hasSize(2))) // Ensure two errors exist
                                 .andExpect(jsonPath("$.errors", containsInAnyOrder(
                                                 "Name cannot be blank",
@@ -174,12 +174,12 @@ void createEmptyNameUserTest() throws Exception {
         }
 
         @Test
-        @DisplayName("Should return status 400 when creating user with name with more than 50 characters")
+        @DisplayName("Should return status 422 Unprocessable Entity when creating user with name with more than 50 characters")
         void createBigNameUserTest() throws Exception {
                 mockMvc.perform(post("/users")
                                 .contentType(MediaType.APPLICATION_JSON)
                                 .content(objectMapper.writeValueAsString(new User("a".repeat(51)))))
-                                .andExpect(status().isBadRequest())
+                                .andExpect(status().isUnprocessableEntity())
                                 .andExpect(jsonPath("$.message").value("Validation error"))
                                 .andExpect(jsonPath("$.errors").value("Name must be between 3 and 50 characters"));
 
@@ -187,12 +187,12 @@ void createBigNameUserTest() throws Exception {
         }
 
         @Test
-        @DisplayName("Should return status 400 when creating user with name with less than 3 characters")
+        @DisplayName("Should return status 422 Unprocessable Entity when creating user with name with less than 3 characters")
         void createSmallNameUserTest() throws Exception {
                 mockMvc.perform(post("/users")
                                 .contentType(MediaType.APPLICATION_JSON)
                                 .content(objectMapper.writeValueAsString(new User("a"))))
-                                .andExpect(status().isBadRequest())
+                                .andExpect(status().isUnprocessableEntity())
                                 .andExpect(jsonPath("$.message").value("Validation error"))
                                 .andExpect(jsonPath("$.errors").value("Name must be between 3 and 50 characters"));
 
@@ -262,14 +262,14 @@ void testUpdateUserNameAlreadyInUse() throws Exception {
         }
 
         @Test
-        @DisplayName("should return 400 when trying to update user with invalid name")
+        @DisplayName("should return 422 when trying to update user with invalid name")
         void testUpdateUserInvalidName() throws Exception {
                 User invalidUser = new User("");
 
                 mockMvc.perform(patch("/users/{id}", userId)
                                 .contentType(MediaType.APPLICATION_JSON)
                                 .content(objectMapper.writeValueAsString(invalidUser)))
-                                .andExpect(status().isBadRequest())
+                                .andExpect(status().isUnprocessableEntity())
                                 .andExpect(jsonPath("$.message").value("Validation error"))
                                 .andExpect(jsonPath("$.errors", hasSize(2)))
                                 .andExpect(jsonPath("$.errors", containsInAnyOrder(
diff --git a/src/test/java/igym/controllers/WorkoutControllerTest.java b/src/test/java/igym/controllers/WorkoutControllerTest.java
new file mode 100644
index 00000000..6da5e2b5
--- /dev/null
+++ b/src/test/java/igym/controllers/WorkoutControllerTest.java
@@ -0,0 +1,198 @@
+package igym.controllers;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import igym.entities.Exercise;
+import igym.entities.Workout;
+import igym.services.WorkoutService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+@WebMvcTest(WorkoutController.class)
+@ExtendWith(MockitoExtension.class)
+public class WorkoutControllerTest {
+
+        @Autowired
+        private MockMvc mockMvc;
+
+        @SuppressWarnings("removal")
+        @MockBean
+        private WorkoutService workoutService;
+
+        private final ObjectMapper objectMapper = new ObjectMapper();
+
+        private Workout workout;
+        UUID gymId = UUID.randomUUID();
+
+        @BeforeEach
+        void setUp() {
+                Exercise ex = new Exercise();
+                ex.setName("Squat");
+                ex.setWeight(60);
+                ex.setNumReps(10);
+                ex.setNumSets(3);
+
+                workout = new Workout();
+                workout.setName("Leg Day");
+                workout.setExerciseList(List.of(ex));
+
+                ReflectionTestUtils.setField(workout, "id", UUID.randomUUID());
+        }
+
+        @Test
+        @DisplayName("should return status 201 when a valid workout is created")
+        void testCreateWorkoutSuccess() throws Exception {
+                when(workoutService.createWorkout(any(Workout.class), eq(gymId))).thenReturn(workout);
+
+                mockMvc.perform(post("/api/gyms/{gymId}/workouts", gymId, gymId)
+                                .contentType(MediaType.APPLICATION_JSON)
+                                .content(objectMapper.writeValueAsString(workout)))
+                                .andExpect(status().isCreated())
+                                .andExpect(jsonPath("$.name").value(workout.getName()))
+                                .andExpect(jsonPath("$.exerciseList[0].name").value("Squat"));
+
+                verify(workoutService, times(1)).createWorkout(any(Workout.class), eq(gymId));
+        }
+
+        @ParameterizedTest
+        @MethodSource("provideInvalidWorkoutNames")
+        @DisplayName("should return 422 Unprocessable Entity for invalid workout names")
+        void testCreateWorkoutWithInvalidName(String invalidName, Set<String> expectedMessages) throws Exception {
+                Exercise ex = new Exercise();
+                ex.setName("Squat");
+                ex.setWeight(60);
+                ex.setNumReps(10);
+                ex.setNumSets(3);
+
+                Workout invalidWorkout = new Workout();
+                invalidWorkout.setName(invalidName);
+                invalidWorkout.setExerciseList(List.of(ex));
+
+                MvcResult result = mockMvc.perform(post("/api/gyms/{gymId}/workouts", gymId)
+                                .contentType(MediaType.APPLICATION_JSON)
+                                .content(objectMapper.writeValueAsString(invalidWorkout)))
+                                .andExpect(status().isUnprocessableEntity())
+                                .andReturn();
+
+                String responseBody = result.getResponse().getContentAsString();
+                JsonNode json = objectMapper.readTree(responseBody);
+                String actualMessage = json.get("errors").get(0).asText();
+
+                assertTrue(expectedMessages.contains(actualMessage),
+                                "Expected one of " + expectedMessages + " but got: " + actualMessage);
+
+                verify(workoutService, never()).createWorkout(any(Workout.class), eq(gymId));
+        }
+
+        private static Stream<Arguments> provideInvalidWorkoutNames() {
+                return Stream.of(
+                                Arguments.of(null, Set.of("Workout name is required")),
+                                Arguments.of("", Set.of("Workout name is required",
+                                                "Name must be between 3 and 50 characters")),
+                                Arguments.of("A".repeat(51), Set.of("Name must be between 3 and 50 characters")));
+        }
+
+        @Test
+        @DisplayName("should return 422 Unprocessable Entity when workout has no exercises")
+        void testCreateWorkoutWithEmptyExerciseList() throws Exception {
+                workout.setExerciseList(List.of());
+
+                mockMvc.perform(post("/api/gyms/{gymId}/workouts", gymId)
+                                .contentType(MediaType.APPLICATION_JSON)
+                                .content(objectMapper.writeValueAsString(workout)))
+                                .andExpect(status().isUnprocessableEntity());
+
+                verify(workoutService, never()).createWorkout(any(Workout.class), eq(gymId));
+        }
+
+        @ParameterizedTest
+        @MethodSource("provideInvalidExercises")
+        @DisplayName("should return 422 Unprocessable Entity for invalid exercise fields")
+        void testCreateWorkoutWithInvalidExercises(Exercise invalidExercise, Set<String> expectedMessages)
+                        throws Exception {
+                Workout workout = new Workout();
+                workout.setName("Valid Workout");
+                workout.setExerciseList(List.of(invalidExercise));
+
+                MvcResult result = mockMvc.perform(post("/api/gyms/{gymId}/workouts", gymId)
+                                .contentType(MediaType.APPLICATION_JSON)
+                                .content(objectMapper.writeValueAsString(workout)))
+                                .andExpect(status().isUnprocessableEntity())
+                                .andReturn();
+
+                String responseBody = result.getResponse().getContentAsString();
+                JsonNode json = objectMapper.readTree(responseBody);
+                String actualMessage = json.get("errors").get(0).asText();
+
+                assertTrue(expectedMessages.contains(actualMessage),
+                                "Expected one of " + expectedMessages + " but got: " + actualMessage);
+
+                verify(workoutService, never()).createWorkout(any(Workout.class), eq(gymId));
+        }
+
+        private static Stream<Arguments> provideInvalidExercises() {
+                return Stream.of(
+                                Arguments.of(
+                                                createExercise("", 20, 10, 3),
+                                                Set.of("Exercise name is required",
+                                                                "Name must be between 3 and 50 characters")),
+                                Arguments.of(
+                                                createExercise("Pushup", -1, 10, 3),
+                                                Set.of("Weight must be zero or positive")),
+                                Arguments.of(
+                                                createExercise("Pushup", 20, 0, 3),
+                                                Set.of("Reps must be at least 1")),
+                                Arguments.of(
+                                                createExercise("Pushup", 20, 10, 0),
+                                                Set.of("Sets must be at least 1")));
+        }
+
+        private static Exercise createExercise(String name, double weight, int reps, int sets) {
+                Exercise ex = new Exercise();
+                ex.setName(name);
+                ex.setWeight(weight);
+                ex.setNumReps(reps);
+                ex.setNumSets(sets);
+                return ex;
+        }
+
+        @Test
+        @DisplayName("should return 500 Internal Server Error when service throws unexpected exception")
+        void testCreateWorkoutInternalError() throws Exception {
+                when(workoutService.createWorkout(any(Workout.class), eq(gymId)))
+                                .thenThrow(new RuntimeException("Unexpected failure"));
+
+                mockMvc.perform(post("/api/gyms/{gymId}/workouts", gymId)
+                                .contentType(MediaType.APPLICATION_JSON)
+                                .content(objectMapper.writeValueAsString(workout)))
+                                .andExpect(status().isInternalServerError());
+
+                verify(workoutService, times(1)).createWorkout(any(Workout.class), eq(gymId));
+        }
+
+}
diff --git a/src/test/java/igym/entities/ExerciseTest.java b/src/test/java/igym/entities/ExerciseTest.java
new file mode 100644
index 00000000..0b9f092a
--- /dev/null
+++ b/src/test/java/igym/entities/ExerciseTest.java
@@ -0,0 +1,97 @@
+package igym.entities;
+
+import jakarta.validation.*;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Set;
+
+import org.junit.jupiter.api.*;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.NullSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class ExerciseTest {
+
+    private static Validator validator;
+
+    @BeforeAll
+    static void init() {
+        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
+        validator = factory.getValidator();
+    }
+    
+    @Test
+    @DisplayName("Test valid exercise") 
+    void testValidExercise() {
+        Exercise ex = new Exercise();
+        ex.setName("Bench Press");
+        ex.setWeight(60);
+        ex.setNumReps(10);
+        ex.setNumSets(4);
+        ex.setNote("Upper body");
+
+        Set<ConstraintViolation<Exercise>> violations = validator.validate(ex);
+        assertTrue(violations.isEmpty(), "Expected no violations for a valid exercise");
+    }
+
+    @ParameterizedTest
+    @NullSource
+    @ValueSource(strings = { "", "AB", "Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }) // 51 chars
+    @DisplayName("Test invalid exercise names")
+    void testInvalidExerciseNames(String invalidName) {
+        Exercise ex = new Exercise();
+        ex.setName(invalidName);
+        ex.setWeight(40);
+        ex.setNumReps(10);
+        ex.setNumSets(3);
+
+        Set<ConstraintViolation<Exercise>> violations = validator.validate(ex);
+        assertFalse(violations.isEmpty(), "Expected violation for name: " + invalidName);
+    }
+
+    @ParameterizedTest
+    @ValueSource(doubles = { -10, -0.1 })
+    @DisplayName("Test invalid exercise weights")
+    void testInvalidWeights(double invalidWeight) {
+        Exercise ex = new Exercise();
+        ex.setName("Deadlift");
+        ex.setWeight(invalidWeight);
+        ex.setNumReps(10);
+        ex.setNumSets(3);
+
+        Set<ConstraintViolation<Exercise>> violations = validator.validate(ex);
+        assertFalse(violations.isEmpty(), "Expected violation for weight: " + invalidWeight);
+    }
+
+    @ParameterizedTest
+    @ValueSource(ints = { 0, -1 })
+    @DisplayName("Test invalid number of repetitions")
+    void testInvalidNumReps(int invalidReps) {
+        Exercise ex = new Exercise();
+        ex.setName("Push-up");
+        ex.setWeight(0);
+        ex.setNumReps(invalidReps);
+        ex.setNumSets(3);
+
+        Set<ConstraintViolation<Exercise>> violations = validator.validate(ex);
+        assertFalse(violations.isEmpty(), "Expected violation for reps: " + invalidReps);
+    }
+
+    @ParameterizedTest
+    @ValueSource(ints = { 0, -2 })
+    @DisplayName("Test invalid number of sets")
+    void testInvalidNumSets(int invalidSets) {
+        Exercise ex = new Exercise();
+        ex.setName("Pull-up");
+        ex.setWeight(0);
+        ex.setNumReps(10);
+        ex.setNumSets(invalidSets);
+
+        Set<ConstraintViolation<Exercise>> violations = validator.validate(ex);
+        assertFalse(violations.isEmpty(), "Expected violation for sets: " + invalidSets);
+    }
+
+}
diff --git a/src/test/java/igym/entities/WorkoutTest.java b/src/test/java/igym/entities/WorkoutTest.java
new file mode 100644
index 00000000..05c4c20c
--- /dev/null
+++ b/src/test/java/igym/entities/WorkoutTest.java
@@ -0,0 +1,77 @@
+package igym.entities;
+
+import jakarta.validation.*;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.*;
+
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class WorkoutTest {
+
+    private static Validator validator;
+
+    @BeforeAll
+    static void init() {
+        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
+        validator = factory.getValidator();
+    }
+
+    private Exercise createValidExercise() {
+        Exercise ex = new Exercise();
+        ex.setName("Squat");
+        ex.setWeight(80);
+        ex.setNumReps(10);
+        ex.setNumSets(4);
+        return ex;
+    }
+
+    @Test
+    @DisplayName("Test valid workout")
+    void testValidWorkout() {
+        Workout workout = new Workout();
+        workout.setName("Upper Body");
+        workout.setExerciseList(List.of(createValidExercise()));
+
+        Set<ConstraintViolation<Workout>> violations = validator.validate(workout);
+        assertTrue(violations.isEmpty(), "Expected no violations for valid workout");
+    }
+
+    @ParameterizedTest
+    @NullSource
+    @ValueSource(strings = { "", "Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }) // 51 chars
+    @DisplayName("Test invalid workout names")
+    void testInvalidWorkoutNames(String invalidName) {
+        Workout workout = new Workout();
+        workout.setName(invalidName);
+        workout.setExerciseList(List.of(createValidExercise()));
+
+        Set<ConstraintViolation<Workout>> violations = validator.validate(workout);
+        assertFalse(violations.isEmpty(), "Expected violation for workout name: '" + invalidName + "'");
+    }
+
+    @Test
+    @DisplayName("Test empty exercise list")
+    void testEmptyExerciseList() {
+        Workout workout = new Workout();
+        workout.setName("Leg Day");
+        workout.setExerciseList(List.of());
+
+        Set<ConstraintViolation<Workout>> violations = validator.validate(workout);
+        assertFalse(violations.isEmpty(), "Expected violation for empty exercise list");
+    }
+
+    @Test
+    @DisplayName("Test null exercise list")
+    void testNullExerciseList() {
+        Workout workout = new Workout();
+        workout.setName("Push Day");
+        workout.setExerciseList(null);
+
+        Set<ConstraintViolation<Workout>> violations = validator.validate(workout);
+        assertFalse(violations.isEmpty(), "Expected violation for null exercise list");
+    }
+
+}
diff --git a/src/test/java/igym/services/GymServiceTest.java b/src/test/java/igym/services/GymServiceTest.java
index cc29ac27..9b25cbbc 100644
--- a/src/test/java/igym/services/GymServiceTest.java
+++ b/src/test/java/igym/services/GymServiceTest.java
@@ -61,7 +61,7 @@ void testAlreadyCreatedGymName() {
 
         assertEquals("A gym with the name 'CrossFit Gym' already exists", exception.getMessage());
         verify(gymRepository, times(1)).existsByName(gym.getName());
-        verify(gymRepository, never()).save(gym);
+        verify(gymRepository, never()).save(any(Gym.class));
     }
 
     @Test
@@ -83,14 +83,13 @@ void testUpdateGym() {
     @DisplayName("Should throw GymNotFoundException when attempting to update a gym that does not exist")
     void testUpdateGymNotFound() {
         UUID gymId = UUID.randomUUID();
-        Gym gym = new Gym("CrossFit Gym");
         String name = "Updated Gym";
         when(gymRepository.findById(gymId)).thenReturn(Optional.empty());
         GymNotFoundException exception = assertThrows(
                 GymNotFoundException.class,
                 () -> gymService.updateGym(gymId, name));
-        assertEquals("Gym with id " + gymId + " not found.", exception.getMessage());
-        verify(gymRepository, never()).save(gym);
+        assertEquals("Gym with id " + gymId + " not found", exception.getMessage());
+        verify(gymRepository, never()).save(any(Gym.class));
     }
 
     @Test
@@ -107,7 +106,7 @@ void testUpdateGymExistingName() {
         assertEquals("A gym with the name '" + gym.getName() + "' already exists.",
                 exception.getMessage());
         verify(gymRepository, times(1)).existsByName(name);
-        verify(gymRepository, never()).save(gym);
+        verify(gymRepository, never()).save(any(Gym.class));
     }
 
     @Test
@@ -156,7 +155,7 @@ void testDeleteGymNotFound() {
 
     @Test
     @DisplayName("should throw GymAlreadyDeletedException when attempting to delete a inactive gym")
-    void testDeleteInactiveGym(){
+    void testDeleteInactiveGym() {
         UUID gymId = UUID.randomUUID();
         Gym gym = new Gym("CrossFit Gym");
         ReflectionTestUtils.setField(gym, "id", gymId);
@@ -168,4 +167,40 @@ void testDeleteInactiveGym(){
         verify(gymRepository, times(1)).findById(gymId);
     }
 
+    @Test
+    @DisplayName("should find a gym by its ID")
+    void testFindById() {
+        UUID gymId = UUID.randomUUID();
+        Gym gym = new Gym("CrossFit Gym");
+        ReflectionTestUtils.setField(gym, "id", gymId);
+        when(gymRepository.findById(gymId)).thenReturn(Optional.of(gym));
+        Gym result = gymService.findById(gymId);
+        assertThat(result).isNotNull();
+        assertThat(result.getName()).isEqualTo("CrossFit Gym");
+    }
+
+    @Test
+    @DisplayName("should throw GymNotFoundException when attempting to find a gym that does not exist")
+    void testFindByIdNotFound() {
+        UUID gymId = UUID.randomUUID();
+        when(gymRepository.findById(gymId)).thenReturn(Optional.empty());
+        GymNotFoundException exception = assertThrows(
+                GymNotFoundException.class,
+                () -> gymService.findById(gymId));
+        assertEquals("Gym with id " + gymId + " not found", exception.getMessage());
+    }
+    
+    @Test
+    @DisplayName("should throw GymNotFoundException when attempting to find a inactive gym")
+    void testFindInactiveGym() {
+        UUID gymId = UUID.randomUUID();
+        Gym gym = new Gym("CrossFit Gym");
+        ReflectionTestUtils.setField(gym, "id", gymId);
+        gym.setStatus(Status.inactive);
+        when(gymRepository.findById(gymId)).thenReturn(Optional.of(gym));
+        GymNotFoundException exception = assertThrows(
+                GymNotFoundException.class,
+                () -> gymService.findById(gymId));
+        assertEquals("Gym with id " + gymId + " not found", exception.getMessage());
+    }
 }
\ No newline at end of file
diff --git a/src/test/java/igym/services/WorkoutServiceTest.java b/src/test/java/igym/services/WorkoutServiceTest.java
new file mode 100644
index 00000000..8950df00
--- /dev/null
+++ b/src/test/java/igym/services/WorkoutServiceTest.java
@@ -0,0 +1,83 @@
+package igym.services;
+
+import igym.entities.Exercise;
+import igym.entities.Gym;
+import igym.entities.Workout;
+import igym.exceptions.GymNotFoundException;
+import igym.repositories.WorkoutRepository;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import java.util.List;
+import java.util.UUID;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+@SpringBootTest
+class WorkoutServiceTest {
+
+    @Mock
+    private WorkoutRepository workoutRepository;
+
+    @Mock
+    private GymService gymService;
+
+    @InjectMocks
+    private WorkoutService workoutService;
+
+    UUID gymId = UUID.randomUUID();
+
+    @Test
+    @DisplayName("Test valid workout creation")
+    void testCreateWorkout() {
+        Exercise ex = new Exercise();
+        ex.setName("Squat");
+        ex.setWeight(60);
+        ex.setNumReps(10);
+        ex.setNumSets(4);
+
+        Workout workout = new Workout();
+        workout.setName("Leg Day");
+        workout.setExerciseList(List.of(ex));
+
+        when(gymService.findById(gymId)).thenReturn(new Gym("Test Gym"));
+        when(workoutRepository.save(any(Workout.class))).thenReturn(workout);
+
+        Workout result = workoutService.createWorkout(workout, gymId);
+
+        assertThat(result).isNotNull();
+        assertThat(result.getName()).isEqualTo("Leg Day");
+        verify(workoutRepository, times(1)).save(any(Workout.class));
+    }
+
+    @Test
+    @DisplayName("Should return an exeception when gym not found in workout creation")
+    void testGymNotFoundInWorkoutCreation() {
+        Exercise ex = new Exercise();
+        ex.setName("Squat");
+        ex.setWeight(60);
+        ex.setNumReps(10);
+        ex.setNumSets(4);
+
+        Workout workout = new Workout();
+        workout.setName("Leg Day");
+        workout.setExerciseList(List.of(ex));
+
+        when(gymService.findById(gymId)).thenThrow(new GymNotFoundException("Gym with id " + gymId + " not found"));
+
+        GymNotFoundException exception = assertThrows(
+                GymNotFoundException.class,
+                () -> workoutService.createWorkout(workout, gymId));
+
+        assertEquals("Gym with id " + gymId + " not found", exception.getMessage());
+        verify(gymService, times(1)).findById(gymId);
+        verify(workoutRepository, never()).save(any());
+    }
+}


Reviews:
user: login: itaayp
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-18T17:13:35Z
user: login: itaayp
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-18T17:14:35Z
user: login: itaayp
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-18T17:27:46Z
user: login: tomerpeceniski
body: Very good job! A lot of work here. Congrats

About the javadoc, I did not find the 'gh-pages' branch. Should I be able to see it?
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-19T10:57:05Z
user: login: danssaul
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-20T12:30:16Z
user: login: danssaul
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-20T12:30:40Z
user: login: danssaul
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-20T16:02:59Z
user: login: danssaul
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-20T16:13:23Z
user: login: danssaul
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-20T16:14:47Z
user: login: danssaul
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-20T16:17:10Z
user: login: tomerpeceniski
body: Nice code! I made just some small suggetions
state: APPROVED
author_association: OWNER
submitted_at: 2025-04-21T12:13:03Z
user: login: danssaul
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-21T13:33:18Z

Review comments:
diff_hunk: @@ -0,0 +1,154 @@
+package igym.controllers;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import igym.entities.Exercise;
+import igym.entities.Workout;
+import igym.services.WorkoutService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Stream;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+@WebMvcTest(WorkoutController.class)
+@ExtendWith(MockitoExtension.class)
+public class WorkoutControllerTest {
+
+    @Autowired
+    private MockMvc mockMvc;
+
+    @SuppressWarnings("removal")
+    @MockBean
+    private WorkoutService workoutService;
+
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    private Workout workout;
+
+    @BeforeEach
+    void setUp() {
+        Exercise ex = new Exercise();
+        ex.setName("Squat");
+        ex.setWeight(60);
+        ex.setNumReps(10);
+        ex.setNumSets(3);
+
+        workout = new Workout();
+        workout.setWorkoutName("Leg Day");
+        workout.setExerciseList(List.of(ex));
+
+        ReflectionTestUtils.setField(workout, "id", UUID.randomUUID());
+    }
+
+    @Test
+    @DisplayName("should return status 201 when a valid workout is created")
+    void testCreateWorkoutSuccess() throws Exception {
+        when(workoutService.createWorkout(any(Workout.class))).thenReturn(workout);
+
+        mockMvc.perform(post("/api/workouts")
+                .contentType(MediaType.APPLICATION_JSON)
+                .content(objectMapper.writeValueAsString(workout)))
+                .andExpect(status().isCreated())
+                .andExpect(jsonPath("$.workoutName").value(workout.getWorkoutName()))
+                .andExpect(jsonPath("$.exerciseList[0].name").value("Squat"));
+
+        verify(workoutService, times(1)).createWorkout(any(Workout.class));
+    }
+
+    @ParameterizedTest
+    @MethodSource("provideInvalidWorkoutNames")
+    @DisplayName("should return 400 Bad Request for invalid workout names")
+    void testCreateWorkoutWithInvalidName(String invalidName) throws Exception {
+        Exercise ex = new Exercise();
+        ex.setName("Squat");
+        ex.setWeight(60);
+        ex.setNumReps(10);
+        ex.setNumSets(3);
+
+        Workout invalidWorkout = new Workout();
+        invalidWorkout.setWorkoutName(invalidName);
+        invalidWorkout.setExerciseList(List.of(ex));
+
+        mockMvc.perform(post("/api/workouts")
+                .contentType(MediaType.APPLICATION_JSON)
+                .content(objectMapper.writeValueAsString(invalidWorkout)))
+                .andExpect(status().isBadRequest());
+
+        verify(workoutService, never()).createWorkout(any(Workout.class));
+    }
+
+    private static Stream<String> provideInvalidWorkoutNames() {
+        return Stream.of(null, "", "  ", "AB", "A".repeat(51));
+    }
+
+    @Test
+    @DisplayName("should return 400 Bad Request when workout has no exercises")
+    void testCreateWorkoutWithEmptyExerciseList() throws Exception {
+        workout.setExerciseList(List.of());
+
+        mockMvc.perform(post("/api/workouts")
+                .contentType(MediaType.APPLICATION_JSON)
+                .content(objectMapper.writeValueAsString(workout)))
+                .andExpect(status().isBadRequest());
+
+        verify(workoutService, never()).createWorkout(any(Workout.class));
+    }
+
+    @ParameterizedTest
+    @MethodSource("provideInvalidExercises")
+    @DisplayName("should return 400 Bad Request for invalid exercise fields")
+    void testCreateWorkoutWithInvalidExercises(Exercise invalidExercise) throws Exception {
+        Workout workout = new Workout();
+        workout.setWorkoutName("Valid Workout");
+        workout.setExerciseList(List.of(invalidExercise));
+    
+        mockMvc.perform(post("/api/workouts")
+                .contentType(MediaType.APPLICATION_JSON)
+                .content(objectMapper.writeValueAsString(workout)))
+                .andExpect(status().isBadRequest());
path: src/test/java/igym/controllers/WorkoutControllerTest.java
user: login: itaayp
body: Daniel, in this case is interesting that the endpoint returns 422 status code, and that the response body describe wich fields are invalid.
created_at: 2025-04-18T17:13:34Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,26 @@
+package igym.services;
+
+import igym.entities.Workout;
+import igym.repositories.WorkoutRepository;
+import jakarta.transaction.Transactional;
+import org.springframework.stereotype.Service;
+
+@Service
+public class WorkoutService {
+
+    private final WorkoutRepository workoutRepository;
+
+    public WorkoutService(WorkoutRepository workoutRepository) {
+        this.workoutRepository = workoutRepository;
+    }
+
+    /**
+     * Creates a new workout with its list of exercises.
+     * @param workout the workout entity to save
+     * @return the saved workout
+     */
path: src/main/java/igym/services/WorkoutService.java
user: login: itaayp
body: 🚀 
created_at: 2025-04-18T17:14:35Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,35 @@
+package igym.entities;
+
+import jakarta.persistence.*;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.*;
+import lombok.*;
+
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Represents a workout plan which contains multiple exercises.
+ */
+@Entity
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class Workout {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.UUID)
+    private UUID id;
+
+    @NotBlank(message = "Workout name is required")
+    @Column(nullable = false, length = 50)
+    @Size(min = 3, max = 50, message = "Name must be between 3 and 50 characters")
+    private String workoutName;
path: src/main/java/igym/entities/Workout.java
user: login: itaayp
body: Maybe the min must be 1, so the user can name its workouts "A", "B", "C", ...
created_at: 2025-04-18T17:17:58Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,77 @@
+package igym.entities;
+
+import jakarta.validation.*;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.*;
+
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class WorkoutTest {
+
+    private static Validator validator;
+
+    @BeforeAll
+    static void init() {
+        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
+        validator = factory.getValidator();
+    }
+
+    private Exercise createValidExercise() {
+        Exercise ex = new Exercise();
+        ex.setName("Squat");
+        ex.setWeight(80);
+        ex.setNumReps(10);
+        ex.setNumSets(4);
+        return ex;
+    }
+
+    @Test
+    @DisplayName("Test valid workout")
+    void testValidWorkout() {
+        Workout workout = new Workout();
+        workout.setWorkoutName("Upper Body");
+        workout.setExerciseList(List.of(createValidExercise()));
+
+        Set<ConstraintViolation<Workout>> violations = validator.validate(workout);
+        assertTrue(violations.isEmpty(), "Expected no violations for valid workout");
+    }
+
+    @ParameterizedTest
+    @NullSource
+    @ValueSource(strings = { "", "AB", "Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }) // 51 chars
+    @DisplayName("Test invalid workout names")
+    void testInvalidWorkoutNames(String invalidName) {
+        Workout workout = new Workout();
+        workout.setWorkoutName(invalidName);
+        workout.setExerciseList(List.of(createValidExercise()));
+
+        Set<ConstraintViolation<Workout>> violations = validator.validate(workout);
+        assertFalse(violations.isEmpty(), "Expected violation for workout name: '" + invalidName + "'");
+    }
+
+    @Test
+    @DisplayName("Test valid exercise list")
path: src/test/java/igym/entities/WorkoutTest.java
user: login: itaayp
body: ```suggestion
    @DisplayName("Test empty exercise list")
```
created_at: 2025-04-18T17:27:06Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,77 @@
+package igym.entities;
+
+import jakarta.validation.*;
+import org.junit.jupiter.api.*;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.*;
+
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class WorkoutTest {
+
+    private static Validator validator;
+
+    @BeforeAll
+    static void init() {
+        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
+        validator = factory.getValidator();
+    }
+
+    private Exercise createValidExercise() {
+        Exercise ex = new Exercise();
+        ex.setName("Squat");
+        ex.setWeight(80);
+        ex.setNumReps(10);
+        ex.setNumSets(4);
+        return ex;
+    }
+
+    @Test
+    @DisplayName("Test valid workout")
+    void testValidWorkout() {
+        Workout workout = new Workout();
+        workout.setWorkoutName("Upper Body");
+        workout.setExerciseList(List.of(createValidExercise()));
+
+        Set<ConstraintViolation<Workout>> violations = validator.validate(workout);
+        assertTrue(violations.isEmpty(), "Expected no violations for valid workout");
+    }
+
+    @ParameterizedTest
+    @NullSource
+    @ValueSource(strings = { "", "AB", "Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }) // 51 chars
+    @DisplayName("Test invalid workout names")
+    void testInvalidWorkoutNames(String invalidName) {
+        Workout workout = new Workout();
+        workout.setWorkoutName(invalidName);
+        workout.setExerciseList(List.of(createValidExercise()));
+
+        Set<ConstraintViolation<Workout>> violations = validator.validate(workout);
+        assertFalse(violations.isEmpty(), "Expected violation for workout name: '" + invalidName + "'");
+    }
+
+    @Test
+    @DisplayName("Test valid exercise list")
+    void testEmptyExerciseList() {
+        Workout workout = new Workout();
+        workout.setWorkoutName("Leg Day");
+        workout.setExerciseList(List.of());
+
+        Set<ConstraintViolation<Workout>> violations = validator.validate(workout);
+        assertFalse(violations.isEmpty(), "Expected violation for empty exercise list");
+    }
+
+    @Test
+    @DisplayName("Test valid exercise list")
path: src/test/java/igym/entities/WorkoutTest.java
user: login: itaayp
body: ```suggestion
    @DisplayName("Test null exercise list")
```
created_at: 2025-04-18T17:27:19Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,26 @@
+package igym.services;
+
+import igym.entities.Workout;
+import igym.repositories.WorkoutRepository;
+import jakarta.transaction.Transactional;
+import org.springframework.stereotype.Service;
+
+@Service
+public class WorkoutService {
+
+    private final WorkoutRepository workoutRepository;
+
+    public WorkoutService(WorkoutRepository workoutRepository) {
+        this.workoutRepository = workoutRepository;
+    }
+
+    /**
+     * Creates a new workout with its list of exercises.
+     * @param workout the workout entity to save
+     * @return the saved workout
+     */
+    @Transactional
+    public Workout createWorkout(Workout workout) {
+        return workoutRepository.save(workout);
path: src/main/java/igym/services/WorkoutService.java
user: login: tomerpeceniski
body: I think it would be nice to verify if there is a workout with that name, before allowing creation
created_at: 2025-04-19T10:26:50Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,35 @@
+package igym.entities;
+
+import jakarta.persistence.*;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.*;
+import lombok.*;
+
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Represents a workout plan which contains multiple exercises.
+ */
+@Entity
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class Workout {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.UUID)
+    private UUID id;
+
+    @NotBlank(message = "Workout name is required")
+    @Column(nullable = false, length = 50)
+    @Size(min = 3, max = 50, message = "Name must be between 3 and 50 characters")
+    private String workoutName;
path: src/main/java/igym/entities/Workout.java
user: login: tomerpeceniski
body: Agree!
created_at: 2025-04-19T10:29:03Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,97 @@
+package igym.entities;
+
+import jakarta.validation.*;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Set;
+
+import org.junit.jupiter.api.*;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.NullSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class ExerciseTest {
+
+    private static Validator validator;
+
+    @BeforeAll
+    static void init() {
+        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
+        validator = factory.getValidator();
+    }
+    
+    @Test
+    @DisplayName("Test valid exercise") 
+    void testValidExercise() {
+        Exercise ex = new Exercise();
+        ex.setName("Bench Press");
+        ex.setWeight(60);
+        ex.setNumReps(10);
+        ex.setNumSets(4);
+        ex.setNote("Upper body");
+
+        Set<ConstraintViolation<Exercise>> violations = validator.validate(ex);
+        assertTrue(violations.isEmpty(), "Expected no violations for a valid exercise");
+    }
+
+    @ParameterizedTest
+    @NullSource
+    @ValueSource(strings = { "", "AB", "Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }) // 51 chars
+    @DisplayName("Test invalid exercise names")
+    void testInvalidExerciseNames(String invalidName) {
+        Exercise ex = new Exercise();
+        ex.setName(invalidName);
+        ex.setWeight(40);
+        ex.setNumReps(10);
+        ex.setNumSets(3);
+
+        Set<ConstraintViolation<Exercise>> violations = validator.validate(ex);
+        assertFalse(violations.isEmpty(), "Expected violation for name: " + invalidName);
+    }
+
+    @ParameterizedTest
+    @ValueSource(doubles = { -10, -0.1 })
+    @DisplayName("Test invalid exercise weights")
+    void testInvalidWeights(double invalidWeight) {
+        Exercise ex = new Exercise();
+        ex.setName("Deadlift");
+        ex.setWeight(invalidWeight);
+        ex.setNumReps(10);
+        ex.setNumSets(3);
+
+        Set<ConstraintViolation<Exercise>> violations = validator.validate(ex);
+        assertFalse(violations.isEmpty(), "Expected violation for weight: " + invalidWeight);
+    }
+
+    @ParameterizedTest
+    @ValueSource(ints = { 0, -1 })
path: src/test/java/igym/entities/ExerciseTest.java
user: login: tomerpeceniski
body: I think it's good to test also double values here
created_at: 2025-04-19T10:31:24Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,97 @@
+package igym.entities;
+
+import jakarta.validation.*;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Set;
+
+import org.junit.jupiter.api.*;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.NullSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class ExerciseTest {
+
+    private static Validator validator;
+
+    @BeforeAll
+    static void init() {
+        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
+        validator = factory.getValidator();
+    }
+    
+    @Test
+    @DisplayName("Test valid exercise") 
+    void testValidExercise() {
+        Exercise ex = new Exercise();
+        ex.setName("Bench Press");
+        ex.setWeight(60);
+        ex.setNumReps(10);
+        ex.setNumSets(4);
+        ex.setNote("Upper body");
+
+        Set<ConstraintViolation<Exercise>> violations = validator.validate(ex);
+        assertTrue(violations.isEmpty(), "Expected no violations for a valid exercise");
+    }
+
+    @ParameterizedTest
+    @NullSource
+    @ValueSource(strings = { "", "AB", "Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }) // 51 chars
+    @DisplayName("Test invalid exercise names")
+    void testInvalidExerciseNames(String invalidName) {
+        Exercise ex = new Exercise();
+        ex.setName(invalidName);
+        ex.setWeight(40);
+        ex.setNumReps(10);
+        ex.setNumSets(3);
+
+        Set<ConstraintViolation<Exercise>> violations = validator.validate(ex);
+        assertFalse(violations.isEmpty(), "Expected violation for name: " + invalidName);
+    }
+
+    @ParameterizedTest
+    @ValueSource(doubles = { -10, -0.1 })
+    @DisplayName("Test invalid exercise weights")
+    void testInvalidWeights(double invalidWeight) {
+        Exercise ex = new Exercise();
+        ex.setName("Deadlift");
+        ex.setWeight(invalidWeight);
+        ex.setNumReps(10);
+        ex.setNumSets(3);
+
+        Set<ConstraintViolation<Exercise>> violations = validator.validate(ex);
+        assertFalse(violations.isEmpty(), "Expected violation for weight: " + invalidWeight);
+    }
+
+    @ParameterizedTest
+    @ValueSource(ints = { 0, -1 })
+    @DisplayName("Test invalid number of repetitions")
+    void testInvalidNumReps(int invalidReps) {
+        Exercise ex = new Exercise();
+        ex.setName("Push-up");
+        ex.setWeight(0);
+        ex.setNumReps(invalidReps);
+        ex.setNumSets(3);
+
+        Set<ConstraintViolation<Exercise>> violations = validator.validate(ex);
+        assertFalse(violations.isEmpty(), "Expected violation for reps: " + invalidReps);
+    }
+
+    @ParameterizedTest
+    @ValueSource(ints = { 0, -2 })
path: src/test/java/igym/entities/ExerciseTest.java
user: login: tomerpeceniski
body: Also test double values here
created_at: 2025-04-19T10:31:39Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,39 @@
+package igym.entities;
+
+import jakarta.persistence.*;
+import jakarta.validation.constraints.*;
+import lombok.*;
+
+import java.util.UUID;
+
+/**
+ * Represents an exercise within a workout.
+ */
+@Entity
+@Getter
+@Setter
path: src/main/java/igym/entities/Exercise.java
user: login: tomerpeceniski
body: we need to find out how to exclude id from this 'Setter' anotation. Because the way it is written, it is creating a setId() method. But we don't want it
created_at: 2025-04-19T10:37:59Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,35 @@
+package igym.entities;
+
+import jakarta.persistence.*;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.*;
+import lombok.*;
+
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Represents a workout plan which contains multiple exercises.
+ */
+@Entity
+@Getter
+@Setter
path: src/main/java/igym/entities/Workout.java
user: login: tomerpeceniski
body: we need to find out how to exclude id from this 'Setter' anotation. Because the way it is written, it is creating a setId() method. But we don't want it
created_at: 2025-04-19T10:38:05Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,35 @@
+package igym.entities;
+
+import jakarta.persistence.*;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.*;
+import lombok.*;
+
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Represents a workout plan which contains multiple exercises.
+ */
+@Entity
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class Workout {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.UUID)
+    private UUID id;
+
+    @NotBlank(message = "Workout name is required")
+    @Column(nullable = false, length = 50)
+    @Size(min = 3, max = 50, message = "Name must be between 3 and 50 characters")
+    private String workoutName;
path: src/main/java/igym/entities/Workout.java
user: login: tomerpeceniski
body: 
```suggestion
    private String name;
```
created_at: 2025-04-19T10:43:08Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,39 @@
+package igym.entities;
+
+import jakarta.persistence.*;
+import jakarta.validation.constraints.*;
+import lombok.*;
+
+import java.util.UUID;
+
+/**
+ * Represents an exercise within a workout.
+ */
+@Entity
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor // Optional if you want to create it with all fields
path: src/main/java/igym/entities/Exercise.java
user: login: tomerpeceniski
body: We need to create constructor without 'id' argument. This way we are allowing id to be passed as an argument in the constructor
created_at: 2025-04-19T10:54:27Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,35 @@
+package igym.entities;
+
+import jakarta.persistence.*;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.*;
+import lombok.*;
+
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Represents a workout plan which contains multiple exercises.
+ */
+@Entity
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
path: src/main/java/igym/entities/Workout.java
user: login: tomerpeceniski
body: We need to create constructor without 'id' argument. This way we are allowing id to be passed as an argument in the constructor
created_at: 2025-04-19T10:54:35Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,154 @@
+package igym.controllers;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import igym.entities.Exercise;
+import igym.entities.Workout;
+import igym.services.WorkoutService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Stream;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+@WebMvcTest(WorkoutController.class)
+@ExtendWith(MockitoExtension.class)
+public class WorkoutControllerTest {
+
+    @Autowired
+    private MockMvc mockMvc;
+
+    @SuppressWarnings("removal")
+    @MockBean
+    private WorkoutService workoutService;
+
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    private Workout workout;
+
+    @BeforeEach
+    void setUp() {
+        Exercise ex = new Exercise();
+        ex.setName("Squat");
+        ex.setWeight(60);
+        ex.setNumReps(10);
+        ex.setNumSets(3);
+
+        workout = new Workout();
+        workout.setWorkoutName("Leg Day");
+        workout.setExerciseList(List.of(ex));
+
+        ReflectionTestUtils.setField(workout, "id", UUID.randomUUID());
+    }
+
+    @Test
+    @DisplayName("should return status 201 when a valid workout is created")
+    void testCreateWorkoutSuccess() throws Exception {
+        when(workoutService.createWorkout(any(Workout.class))).thenReturn(workout);
+
+        mockMvc.perform(post("/api/workouts")
+                .contentType(MediaType.APPLICATION_JSON)
+                .content(objectMapper.writeValueAsString(workout)))
+                .andExpect(status().isCreated())
+                .andExpect(jsonPath("$.workoutName").value(workout.getWorkoutName()))
+                .andExpect(jsonPath("$.exerciseList[0].name").value("Squat"));
+
+        verify(workoutService, times(1)).createWorkout(any(Workout.class));
+    }
+
+    @ParameterizedTest
+    @MethodSource("provideInvalidWorkoutNames")
+    @DisplayName("should return 400 Bad Request for invalid workout names")
+    void testCreateWorkoutWithInvalidName(String invalidName) throws Exception {
+        Exercise ex = new Exercise();
+        ex.setName("Squat");
+        ex.setWeight(60);
+        ex.setNumReps(10);
+        ex.setNumSets(3);
+
+        Workout invalidWorkout = new Workout();
+        invalidWorkout.setWorkoutName(invalidName);
+        invalidWorkout.setExerciseList(List.of(ex));
+
+        mockMvc.perform(post("/api/workouts")
+                .contentType(MediaType.APPLICATION_JSON)
+                .content(objectMapper.writeValueAsString(invalidWorkout)))
+                .andExpect(status().isBadRequest());
+
+        verify(workoutService, never()).createWorkout(any(Workout.class));
+    }
+
+    private static Stream<String> provideInvalidWorkoutNames() {
+        return Stream.of(null, "", "  ", "AB", "A".repeat(51));
+    }
+
+    @Test
+    @DisplayName("should return 400 Bad Request when workout has no exercises")
+    void testCreateWorkoutWithEmptyExerciseList() throws Exception {
+        workout.setExerciseList(List.of());
+
+        mockMvc.perform(post("/api/workouts")
+                .contentType(MediaType.APPLICATION_JSON)
+                .content(objectMapper.writeValueAsString(workout)))
+                .andExpect(status().isBadRequest());
+
+        verify(workoutService, never()).createWorkout(any(Workout.class));
+    }
+
+    @ParameterizedTest
+    @MethodSource("provideInvalidExercises")
+    @DisplayName("should return 400 Bad Request for invalid exercise fields")
+    void testCreateWorkoutWithInvalidExercises(Exercise invalidExercise) throws Exception {
+        Workout workout = new Workout();
+        workout.setWorkoutName("Valid Workout");
+        workout.setExerciseList(List.of(invalidExercise));
+    
+        mockMvc.perform(post("/api/workouts")
+                .contentType(MediaType.APPLICATION_JSON)
+                .content(objectMapper.writeValueAsString(workout)))
+                .andExpect(status().isBadRequest());
path: src/test/java/igym/controllers/WorkoutControllerTest.java
user: login: danssaul
body: very nice your observation 
created_at: 2025-04-20T12:30:16Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,35 @@
+package igym.entities;
+
+import jakarta.persistence.*;
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.*;
+import lombok.*;
+
+import java.util.List;
+import java.util.UUID;
+
+/**
+ * Represents a workout plan which contains multiple exercises.
+ */
+@Entity
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+@Builder
+public class Workout {
+
+    @Id
+    @GeneratedValue(strategy = GenerationType.UUID)
+    private UUID id;
+
+    @NotBlank(message = "Workout name is required")
+    @Column(nullable = false, length = 50)
+    @Size(min = 3, max = 50, message = "Name must be between 3 and 50 characters")
+    private String workoutName;
path: src/main/java/igym/entities/Workout.java
user: login: danssaul
body: thank you bros
created_at: 2025-04-20T12:30:40Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,26 @@
+package igym.services;
+
+import igym.entities.Workout;
+import igym.repositories.WorkoutRepository;
+import jakarta.transaction.Transactional;
+import org.springframework.stereotype.Service;
+
+@Service
+public class WorkoutService {
+
+    private final WorkoutRepository workoutRepository;
+
+    public WorkoutService(WorkoutRepository workoutRepository) {
+        this.workoutRepository = workoutRepository;
+    }
+
+    /**
+     * Creates a new workout with its list of exercises.
+     * @param workout the workout entity to save
+     * @return the saved workout
+     */
+    @Transactional
+    public Workout createWorkout(Workout workout) {
+        return workoutRepository.save(workout);
path: src/main/java/igym/services/WorkoutService.java
user: login: danssaul
body: I though to let this resposability to the user, I don't know but maybe someone can wants to workouts with the same name 
created_at: 2025-04-20T16:02:59Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,97 @@
+package igym.entities;
+
+import jakarta.validation.*;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Set;
+
+import org.junit.jupiter.api.*;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.NullSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class ExerciseTest {
+
+    private static Validator validator;
+
+    @BeforeAll
+    static void init() {
+        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
+        validator = factory.getValidator();
+    }
+    
+    @Test
+    @DisplayName("Test valid exercise") 
+    void testValidExercise() {
+        Exercise ex = new Exercise();
+        ex.setName("Bench Press");
+        ex.setWeight(60);
+        ex.setNumReps(10);
+        ex.setNumSets(4);
+        ex.setNote("Upper body");
+
+        Set<ConstraintViolation<Exercise>> violations = validator.validate(ex);
+        assertTrue(violations.isEmpty(), "Expected no violations for a valid exercise");
+    }
+
+    @ParameterizedTest
+    @NullSource
+    @ValueSource(strings = { "", "AB", "Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }) // 51 chars
+    @DisplayName("Test invalid exercise names")
+    void testInvalidExerciseNames(String invalidName) {
+        Exercise ex = new Exercise();
+        ex.setName(invalidName);
+        ex.setWeight(40);
+        ex.setNumReps(10);
+        ex.setNumSets(3);
+
+        Set<ConstraintViolation<Exercise>> violations = validator.validate(ex);
+        assertFalse(violations.isEmpty(), "Expected violation for name: " + invalidName);
+    }
+
+    @ParameterizedTest
+    @ValueSource(doubles = { -10, -0.1 })
+    @DisplayName("Test invalid exercise weights")
+    void testInvalidWeights(double invalidWeight) {
+        Exercise ex = new Exercise();
+        ex.setName("Deadlift");
+        ex.setWeight(invalidWeight);
+        ex.setNumReps(10);
+        ex.setNumSets(3);
+
+        Set<ConstraintViolation<Exercise>> violations = validator.validate(ex);
+        assertFalse(violations.isEmpty(), "Expected violation for weight: " + invalidWeight);
+    }
+
+    @ParameterizedTest
+    @ValueSource(ints = { 0, -1 })
path: src/test/java/igym/entities/ExerciseTest.java
user: login: danssaul
body: It's int the field, let's make this especific validation to the front, I'll add in the front end task in  notion
created_at: 2025-04-20T16:13:23Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,97 @@
+package igym.entities;
+
+import jakarta.validation.*;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Set;
+
+import org.junit.jupiter.api.*;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.NullSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class ExerciseTest {
+
+    private static Validator validator;
+
+    @BeforeAll
+    static void init() {
+        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
+        validator = factory.getValidator();
+    }
+    
+    @Test
+    @DisplayName("Test valid exercise") 
+    void testValidExercise() {
+        Exercise ex = new Exercise();
+        ex.setName("Bench Press");
+        ex.setWeight(60);
+        ex.setNumReps(10);
+        ex.setNumSets(4);
+        ex.setNote("Upper body");
+
+        Set<ConstraintViolation<Exercise>> violations = validator.validate(ex);
+        assertTrue(violations.isEmpty(), "Expected no violations for a valid exercise");
+    }
+
+    @ParameterizedTest
+    @NullSource
+    @ValueSource(strings = { "", "AB", "Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }) // 51 chars
+    @DisplayName("Test invalid exercise names")
+    void testInvalidExerciseNames(String invalidName) {
+        Exercise ex = new Exercise();
+        ex.setName(invalidName);
+        ex.setWeight(40);
+        ex.setNumReps(10);
+        ex.setNumSets(3);
+
+        Set<ConstraintViolation<Exercise>> violations = validator.validate(ex);
+        assertFalse(violations.isEmpty(), "Expected violation for name: " + invalidName);
+    }
+
+    @ParameterizedTest
+    @ValueSource(doubles = { -10, -0.1 })
+    @DisplayName("Test invalid exercise weights")
+    void testInvalidWeights(double invalidWeight) {
+        Exercise ex = new Exercise();
+        ex.setName("Deadlift");
+        ex.setWeight(invalidWeight);
+        ex.setNumReps(10);
+        ex.setNumSets(3);
+
+        Set<ConstraintViolation<Exercise>> violations = validator.validate(ex);
+        assertFalse(violations.isEmpty(), "Expected violation for weight: " + invalidWeight);
+    }
+
+    @ParameterizedTest
+    @ValueSource(ints = { 0, -1 })
path: src/test/java/igym/entities/ExerciseTest.java
user: login: danssaul
body: we have think as well if are useful validations to the user
created_at: 2025-04-20T16:14:47Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,39 @@
+package igym.entities;
+
+import jakarta.persistence.*;
+import jakarta.validation.constraints.*;
+import lombok.*;
+
+import java.util.UUID;
+
+/**
+ * Represents an exercise within a workout.
+ */
+@Entity
+@Getter
+@Setter
path: src/main/java/igym/entities/Exercise.java
user: login: danssaul
body: you are right, I'll prevent this using a especific annotation to that
created_at: 2025-04-20T16:17:10Z
author_association: COLLABORATOR
diff_hunk: @@ -1,26 +1,47 @@
 package igym.services;
 
+import igym.entities.Exercise;
+import igym.entities.Gym;
 import igym.entities.Workout;
+import igym.exceptions.GymNotFoundException;
+import igym.repositories.GymRepository;
 import igym.repositories.WorkoutRepository;
 import jakarta.transaction.Transactional;
+
+import java.util.UUID;
+
 import org.springframework.stereotype.Service;
 
 @Service
 public class WorkoutService {
 
     private final WorkoutRepository workoutRepository;
+    private final GymRepository gymRepository; 
 
-    public WorkoutService(WorkoutRepository workoutRepository) {
+    public WorkoutService(WorkoutRepository workoutRepository, GymRepository gymRepository) {
         this.workoutRepository = workoutRepository;
+        this.gymRepository = gymRepository;
     }
 
     /**
      * Creates a new workout with its list of exercises.
+     * 
      * @param workout the workout entity to save
      * @return the saved workout
      */
     @Transactional
-    public Workout createWorkout(Workout workout) {
+    public Workout createWorkout(Workout workout, UUID gymId) {
+        Gym gym = gymRepository.findById(gymId)
+                .orElseThrow(() -> new GymNotFoundException("Gym not found"));
path: src/main/java/igym/services/WorkoutService.java
user: login: tomerpeceniski
body: Here wouldn't be better to create a method findById in the gymService and then import the gymService here? So then our gymService is responsable of the business rules of this entity and then we can create a test for it in the gymServiceTest
created_at: 2025-04-21T11:01:20Z
author_association: OWNER
diff_hunk: @@ -8,28 +8,39 @@
 import java.util.List;
 import java.util.UUID;
 
+import com.fasterxml.jackson.annotation.JsonBackReference;
+import com.fasterxml.jackson.annotation.JsonManagedReference;
+
 /**
  * Represents a workout plan which contains multiple exercises.
+ * Lombok's @Setter is applied to the class, but the ID field
+ * uses @Setter(AccessLevel.NONE)
+ * to prevent accidental assignment, as it's auto-generated.
  */
 @Entity
 @Getter
 @Setter
 @NoArgsConstructor
-@AllArgsConstructor
-@Builder
 public class Workout {
 
     @Id
     @GeneratedValue(strategy = GenerationType.UUID)
+    @Setter(AccessLevel.NONE)
     private UUID id;
 
     @NotBlank(message = "Workout name is required")
     @Column(nullable = false, length = 50)
-    @Size(min = 3, max = 50, message = "Name must be between 3 and 50 characters")
-    private String workoutName;
+    @Size(min = 1, max = 50, message = "Name must be between 3 and 50 characters")
+    private String name;
 
     @NotEmpty(message = "Workout must contain at least one exercise")
-    @OneToMany(cascade = CascadeType.ALL)
+    @OneToMany(mappedBy = "workout", cascade = CascadeType.ALL)
     @Valid
+    @JsonManagedReference
     private List<Exercise> exerciseList;
+
+    @ManyToOne(optional = false)
+    @JoinColumn(name = "gym_id", nullable = false)
+    @JsonBackReference
+    private Gym gym;
 }
path: src/main/java/igym/entities/Workout.java
user: login: tomerpeceniski
body: ```suggestion

public Workout(String name, List<Exercise> exerciseList, Gym gym) {
    this.name = name;
    this.exerciseList = exerciseList;
    this.gym = gym;
}

}
```
created_at: 2025-04-21T12:05:11Z
author_association: OWNER
diff_hunk: @@ -8,28 +8,39 @@
 import java.util.List;
 import java.util.UUID;
 
+import com.fasterxml.jackson.annotation.JsonBackReference;
+import com.fasterxml.jackson.annotation.JsonManagedReference;
+
 /**
  * Represents a workout plan which contains multiple exercises.
+ * Lombok's @Setter is applied to the class, but the ID field
+ * uses @Setter(AccessLevel.NONE)
+ * to prevent accidental assignment, as it's auto-generated.
  */
 @Entity
 @Getter
 @Setter
 @NoArgsConstructor
-@AllArgsConstructor
-@Builder
 public class Workout {
 
     @Id
     @GeneratedValue(strategy = GenerationType.UUID)
+    @Setter(AccessLevel.NONE)
     private UUID id;
 
     @NotBlank(message = "Workout name is required")
     @Column(nullable = false, length = 50)
-    @Size(min = 3, max = 50, message = "Name must be between 3 and 50 characters")
-    private String workoutName;
+    @Size(min = 1, max = 50, message = "Name must be between 3 and 50 characters")
+    private String name;
 
     @NotEmpty(message = "Workout must contain at least one exercise")
-    @OneToMany(cascade = CascadeType.ALL)
+    @OneToMany(mappedBy = "workout", cascade = CascadeType.ALL)
     @Valid
+    @JsonManagedReference
     private List<Exercise> exerciseList;
+
+    @ManyToOne(optional = false)
+    @JoinColumn(name = "gym_id", nullable = false)
+    @JsonBackReference
+    private Gym gym;
 }
path: src/main/java/igym/entities/Workout.java
user: login: tomerpeceniski
body: I think we should have also a constructor with arguments (excluding only id)
created_at: 2025-04-21T12:06:03Z
author_association: OWNER
diff_hunk: @@ -36,4 +45,9 @@ public class Exercise {
     private int numSets;
 
     private String note;
+
+    @ManyToOne
+    @JoinColumn(name = "workout_id", nullable = false)
+    @JsonBackReference
+    private Workout workout;
 }
path: src/main/java/igym/entities/Exercise.java
user: login: tomerpeceniski
body: ```suggestion

public Exercise(String name, double weight, int numReps, int numSets, String note, Workout workout) {
    this.name = name;
    this.weight = weight;
    this.numReps = numReps;
    this.numSets = numSets;
    this.note = note;
    this.workout = workout;
}

}
```
created_at: 2025-04-21T12:10:29Z
author_association: OWNER
diff_hunk: @@ -1,26 +1,47 @@
 package igym.services;
 
+import igym.entities.Exercise;
+import igym.entities.Gym;
 import igym.entities.Workout;
+import igym.exceptions.GymNotFoundException;
+import igym.repositories.GymRepository;
 import igym.repositories.WorkoutRepository;
 import jakarta.transaction.Transactional;
+
+import java.util.UUID;
+
 import org.springframework.stereotype.Service;
 
 @Service
 public class WorkoutService {
 
     private final WorkoutRepository workoutRepository;
+    private final GymRepository gymRepository; 
 
-    public WorkoutService(WorkoutRepository workoutRepository) {
+    public WorkoutService(WorkoutRepository workoutRepository, GymRepository gymRepository) {
         this.workoutRepository = workoutRepository;
+        this.gymRepository = gymRepository;
     }
 
     /**
      * Creates a new workout with its list of exercises.
+     * 
      * @param workout the workout entity to save
      * @return the saved workout
      */
     @Transactional
-    public Workout createWorkout(Workout workout) {
+    public Workout createWorkout(Workout workout, UUID gymId) {
+        Gym gym = gymRepository.findById(gymId)
+                .orElseThrow(() -> new GymNotFoundException("Gym not found"));
path: src/main/java/igym/services/WorkoutService.java
user: login: danssaul
body: Yes, let's make the code a bit dryer because we have already used this method in the repo in another services, I'll acept you suggestion
created_at: 2025-04-21T13:33:18Z
author_association: COLLABORATOR

PR comments:
user: login: itaayp
created_at: 2025-04-18T16:52:31Z
body: Nice, Daniel! After the documentation is online, let's add it to our readme
author_association: COLLABORATOR
user: login: itaayp
created_at: 2025-04-18T17:29:40Z
body: Daniel, I miss the link between a Gym with Workouts in your PR. 
I also reviewed the task in Notion, and realize that there isn't any mention there. This is right? The link should be develop in your PR?
author_association: COLLABORATOR
user: login: sonarqubecloud[bot]
created_at: 2025-04-21T13:56:23Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=tomerpeceniski_igym&pullRequest=18) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [4 New issues](https://sonarcloud.io/project/issues?id=tomerpeceniski_igym&pullRequest=18&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=tomerpeceniski_igym&pullRequest=18&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tomerpeceniski_igym&pullRequest=18&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [86.2% Coverage on New Code](https://sonarcloud.io/component_measures?id=tomerpeceniski_igym&pullRequest=18&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=tomerpeceniski_igym&pullRequest=18&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=tomerpeceniski_igym&pullRequest=18)


author_association: NONE