diff --git a/build.gradle.kts b/build.gradle.kts index 515e4c23..3246a6b1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -99,7 +99,6 @@ tasks { xml.required.set(true) // Enable XML report for SonarQube html.required.set(true) // Keep HTML for local viewing csv.required.set(false) // Disable CSV to reduce clutter - html.outputLocation.set(layout.buildDirectory.dir("reports/jacoco/integration")) } } @@ -133,6 +132,7 @@ tasks { } jacocoTestCoverageVerification { + dependsOn("jacocoTestReport", "jacocoIntegrationReport") executionData( fileTree(layout.buildDirectory.asFile.get()).include( "jacoco/test.exec", @@ -270,4 +270,3 @@ tasks.register("pmdAll") { description = "Runs all PMD checks (main, test, testInt)." dependsOn("pmdMain", "pmdTest", "pmdTestInt") } - diff --git a/src/main/java/com/wcc/platform/configuration/GlobalExceptionHandler.java b/src/main/java/com/wcc/platform/configuration/GlobalExceptionHandler.java index d841238f..9a56d58d 100644 --- a/src/main/java/com/wcc/platform/configuration/GlobalExceptionHandler.java +++ b/src/main/java/com/wcc/platform/configuration/GlobalExceptionHandler.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonMappingException.Reference; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; import com.wcc.platform.domain.exceptions.ApplicationMenteeWorkflowException; import com.wcc.platform.domain.exceptions.ApplicationNotFoundException; @@ -32,6 +33,7 @@ import java.util.NoSuchElementException; import java.util.stream.Collectors; import java.util.stream.IntStream; +import java.time.Month; import lombok.extern.slf4j.Slf4j; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; @@ -217,6 +219,13 @@ public ResponseEntity handleForbiddenException( private String extractReadableMessage(final HttpMessageNotReadableException ex) { final var cause = ex.getMostSpecificCause(); + if (cause instanceof InvalidFormatException invalidFormat + && Month.class.equals(invalidFormat.getTargetType()) + && invalidFormat.getPath().stream() + .anyMatch(reference -> "cycleMonth".equals(reference.getFieldName()))) { + return "Cycle month must be between February and November"; + } + if (cause instanceof UnrecognizedPropertyException unrecognizedProperty) { final var allowedFields = unrecognizedProperty.getKnownPropertyIds().stream() diff --git a/src/main/java/com/wcc/platform/controller/MentorshipAdminMatchesController.java b/src/main/java/com/wcc/platform/controller/MentorshipAdminMatchesController.java index 68f85a1b..36220d8a 100644 --- a/src/main/java/com/wcc/platform/controller/MentorshipAdminMatchesController.java +++ b/src/main/java/com/wcc/platform/controller/MentorshipAdminMatchesController.java @@ -2,12 +2,13 @@ import com.wcc.platform.configuration.security.RequiresPermission; import com.wcc.platform.configuration.security.RequiresRole; -import com.wcc.platform.domain.exceptions.CycleNotFoundException; import com.wcc.platform.domain.auth.Permission; +import com.wcc.platform.domain.exceptions.CycleNotFoundException; import com.wcc.platform.domain.platform.mentorship.ApplicationStatus; import com.wcc.platform.domain.platform.mentorship.CycleStatus; import com.wcc.platform.domain.platform.mentorship.MatchCancelRequest; import com.wcc.platform.domain.platform.mentorship.MenteeApplicationAdminResponse; +import com.wcc.platform.domain.platform.mentorship.MentorshipCycleCreateRequest; import com.wcc.platform.domain.platform.mentorship.MentorshipCycleEntity; import com.wcc.platform.domain.platform.mentorship.MentorshipMatch; import com.wcc.platform.domain.platform.mentorship.recommendation.MentorshipRecommendationResponse; @@ -198,6 +199,24 @@ public ResponseEntity incrementSessionCount( // ==================== Cycle Management ==================== + /** + * API to create a mentorship cycle in draft status. + * + * @param request The cycle creation request + * @return Created mentorship cycle + */ + @PostMapping("/cycles") + @RequiresRole({RoleType.ADMIN, RoleType.MENTORSHIP_ADMIN}) + @Operation( + summary = "Create a mentorship cycle", + security = {@SecurityRequirement(name = BEARER_AUTH)}) + @ResponseStatus(HttpStatus.CREATED) + public ResponseEntity createCycle( + @Valid @RequestBody final MentorshipCycleCreateRequest request) { + final MentorshipCycleEntity createdCycle = cycleService.createCycle(request); + return ResponseEntity.status(HttpStatus.CREATED).body(createdCycle); + } + /** * API to get the currently open mentorship cycle. * diff --git a/src/main/java/com/wcc/platform/domain/platform/mentorship/MentorshipCycleCreateRequest.java b/src/main/java/com/wcc/platform/domain/platform/mentorship/MentorshipCycleCreateRequest.java new file mode 100644 index 00000000..ce16158e --- /dev/null +++ b/src/main/java/com/wcc/platform/domain/platform/mentorship/MentorshipCycleCreateRequest.java @@ -0,0 +1,21 @@ +package com.wcc.platform.domain.platform.mentorship; + +import com.wcc.platform.validation.ValidMentorshipCycleMonth; +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDate; +import java.time.Month; +import java.time.Year; + +/** Request payload for creating a mentorship cycle. */ +public record MentorshipCycleCreateRequest( + @NotNull @FutureOrPresent Year cycleYear, + @NotNull @ValidMentorshipCycleMonth Month cycleMonth, + @NotNull MentorshipType mentorshipType, + @NotNull @FutureOrPresent LocalDate registrationStartDate, + @NotNull @FutureOrPresent LocalDate registrationEndDate, + @NotNull @FutureOrPresent LocalDate cycleStartDate, + @FutureOrPresent LocalDate cycleEndDate, + @Min(1) Integer maxMenteesPerMentor, + String description) {} diff --git a/src/main/java/com/wcc/platform/domain/platform/mentorship/MentorshipCycleEntity.java b/src/main/java/com/wcc/platform/domain/platform/mentorship/MentorshipCycleEntity.java index 192d226f..2bc6bec4 100644 --- a/src/main/java/com/wcc/platform/domain/platform/mentorship/MentorshipCycleEntity.java +++ b/src/main/java/com/wcc/platform/domain/platform/mentorship/MentorshipCycleEntity.java @@ -1,6 +1,10 @@ package com.wcc.platform.domain.platform.mentorship; import com.wcc.platform.domain.cms.pages.mentorship.MentorsPage; +import com.wcc.platform.validation.ValidMentorshipCycleMonth; +import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; import java.time.LocalDate; import java.time.Month; import java.time.Year; @@ -16,14 +20,22 @@ @Builder public class MentorshipCycleEntity { private Long cycleId; + @NotNull @FutureOrPresent private Year cycleYear; + @NotNull private MentorshipType mentorshipType; + @NotNull @ValidMentorshipCycleMonth private Month cycleMonth; + @NotNull @FutureOrPresent private LocalDate registrationStartDate; + @NotNull @FutureOrPresent private LocalDate registrationEndDate; + @NotNull @FutureOrPresent private LocalDate cycleStartDate; + @FutureOrPresent private LocalDate cycleEndDate; private CycleStatus status; + @Min(1) private Integer maxMenteesPerMentor; private String description; private ZonedDateTime createdAt; diff --git a/src/main/java/com/wcc/platform/repository/MentorshipCycleRepository.java b/src/main/java/com/wcc/platform/repository/MentorshipCycleRepository.java index 332d0fcf..b351d40c 100644 --- a/src/main/java/com/wcc/platform/repository/MentorshipCycleRepository.java +++ b/src/main/java/com/wcc/platform/repository/MentorshipCycleRepository.java @@ -3,6 +3,7 @@ import com.wcc.platform.domain.platform.mentorship.CycleStatus; import com.wcc.platform.domain.platform.mentorship.MentorshipCycleEntity; import com.wcc.platform.domain.platform.mentorship.MentorshipType; +import java.time.Month; import java.time.Year; import java.util.List; import java.util.Optional; @@ -67,4 +68,7 @@ public interface MentorshipCycleRepository extends CrudRepository findByYearAndTypeAndMonth( + Year year, MentorshipType type, Month month); } diff --git a/src/main/java/com/wcc/platform/repository/postgres/mentorship/PostgresMentorshipCycleRepository.java b/src/main/java/com/wcc/platform/repository/postgres/mentorship/PostgresMentorshipCycleRepository.java index 16413c9d..101e29dd 100644 --- a/src/main/java/com/wcc/platform/repository/postgres/mentorship/PostgresMentorshipCycleRepository.java +++ b/src/main/java/com/wcc/platform/repository/postgres/mentorship/PostgresMentorshipCycleRepository.java @@ -1,5 +1,6 @@ package com.wcc.platform.repository.postgres.mentorship; +import com.wcc.platform.domain.exceptions.DuplicatedException; import com.wcc.platform.domain.platform.mentorship.CycleStatus; import com.wcc.platform.domain.platform.mentorship.MentorshipCycleEntity; import com.wcc.platform.domain.platform.mentorship.MentorshipType; @@ -66,13 +67,25 @@ public class PostgresMentorshipCycleRepository implements MentorshipCycleReposit + "description = ?, updated_at = CURRENT_TIMESTAMP " + "WHERE cycle_id = ?"; + private static final String SELECT_CYCLE_BY_KEY = + "SELECT * FROM mentorship_cycles " + + "WHERE cycle_year = ? AND mentorship_type = ? AND cycle_month = ?"; + private final JdbcTemplate jdbc; @Override public MentorshipCycleEntity create(final MentorshipCycleEntity entity) { - final var existing = findByYearAndType(entity.getCycleYear(), entity.getMentorshipType()); + final var existing = + findByYearAndTypeAndMonth( + entity.getCycleYear(), entity.getMentorshipType(), entity.getCycleMonth()); + if (existing.isPresent()) { - return update(existing.get().getCycleId(), entity); + throw new DuplicatedException( + "Mentorship cycle already exists for year %d, type %s and month %s" + .formatted( + entity.getCycleYear().getValue(), + entity.getMentorshipType().name(), + entity.getCycleMonth().name())); } final Long generatedId = @@ -201,4 +214,15 @@ private MentorshipCycleEntity mapRow(final ResultSet rs) throws SQLException { .updatedAt(rs.getTimestamp("updated_at").toInstant().atZone(ZoneId.systemDefault())) .build(); } + + @Override + public Optional findByYearAndTypeAndMonth( + final Year year, final MentorshipType type, final Month month) { + return jdbc.query( + SELECT_CYCLE_BY_KEY, + rs -> rs.next() ? Optional.of(mapRow(rs)) : Optional.empty(), + year.getValue(), + type.getMentorshipTypeId(), + month.getValue()); + } } diff --git a/src/main/java/com/wcc/platform/service/MentorshipCycleService.java b/src/main/java/com/wcc/platform/service/MentorshipCycleService.java index 9446114f..fb87faa7 100644 --- a/src/main/java/com/wcc/platform/service/MentorshipCycleService.java +++ b/src/main/java/com/wcc/platform/service/MentorshipCycleService.java @@ -3,6 +3,7 @@ import com.wcc.platform.domain.exceptions.CycleNotFoundException; import com.wcc.platform.domain.exceptions.InvalidCycleStatusTransitionException; import com.wcc.platform.domain.platform.mentorship.CycleStatus; +import com.wcc.platform.domain.platform.mentorship.MentorshipCycleCreateRequest; import com.wcc.platform.domain.platform.mentorship.MentorshipCycleEntity; import com.wcc.platform.repository.MentorshipCycleRepository; import jakarta.validation.constraints.FutureOrPresent; @@ -61,4 +62,47 @@ public MentorshipCycleEntity updateStatus(final Long cycleId, final CycleStatus return cycleRepository.updateStatus(cycleId, newStatus); } + + /** + * Creates a new mentorship cycle in draft status. + * + * @param request cycle creation data + * @return the created mentorship cycle + * @throws IllegalArgumentException if the supplied dates are inconsistent + */ + public MentorshipCycleEntity createCycle(final MentorshipCycleCreateRequest request) { + + if (request.registrationEndDate().isBefore(request.registrationStartDate())) { + throw new IllegalArgumentException( + "Registration end date must be greater than or equal to registration start date"); + } + + if (request.cycleStartDate().isBefore(request.registrationStartDate())) { + throw new IllegalArgumentException( + "Cycle start date must be greater than or equal to registration start date"); + } + + if (request.cycleEndDate() != null + && request.cycleEndDate().isBefore(request.cycleStartDate())) { + throw new IllegalArgumentException( + "Cycle end date must be greater than or equal to cycle start date"); + } + + final MentorshipCycleEntity cycle = + MentorshipCycleEntity.builder() + .cycleYear(request.cycleYear()) + .cycleMonth(request.cycleMonth()) + .mentorshipType(request.mentorshipType()) + .registrationStartDate(request.registrationStartDate()) + .registrationEndDate(request.registrationEndDate()) + .cycleStartDate(request.cycleStartDate()) + .cycleEndDate(request.cycleEndDate()) + .status(CycleStatus.DRAFT) + .maxMenteesPerMentor( + request.maxMenteesPerMentor() != null ? request.maxMenteesPerMentor() : 5) + .description(request.description()) + .build(); + + return cycleRepository.create(cycle); + } } diff --git a/src/main/java/com/wcc/platform/validation/MentorshipCycleMonthValidator.java b/src/main/java/com/wcc/platform/validation/MentorshipCycleMonthValidator.java new file mode 100644 index 00000000..450d24f9 --- /dev/null +++ b/src/main/java/com/wcc/platform/validation/MentorshipCycleMonthValidator.java @@ -0,0 +1,15 @@ +package com.wcc.platform.validation; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import java.time.Month; + +/** Validates the allowed month range for mentorship cycles. */ +public class MentorshipCycleMonthValidator + implements ConstraintValidator { + + @Override + public boolean isValid(final Month month, final ConstraintValidatorContext context) { + return month == null || (month.getValue() >= 2 && month.getValue() <= 11); + } +} diff --git a/src/main/java/com/wcc/platform/validation/ValidMentorshipCycleMonth.java b/src/main/java/com/wcc/platform/validation/ValidMentorshipCycleMonth.java new file mode 100644 index 00000000..40518295 --- /dev/null +++ b/src/main/java/com/wcc/platform/validation/ValidMentorshipCycleMonth.java @@ -0,0 +1,30 @@ +package com.wcc.platform.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.lang.annotation.RetentionPolicy; + +/** + * Validates that a mentorship cycle month is between February and November. + */ +@Documented +@Constraint(validatedBy = MentorshipCycleMonthValidator.class) +@Target({ + ElementType.FIELD, + ElementType.PARAMETER, + ElementType.ANNOTATION_TYPE, + ElementType.RECORD_COMPONENT +}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ValidMentorshipCycleMonth { + + String message() default "Cycle month must be between February and November"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/resources/db/migration/V40__20261011__restrict_mentorship_cycle_month.sql b/src/main/resources/db/migration/V40__20261011__restrict_mentorship_cycle_month.sql new file mode 100644 index 00000000..f22c65a8 --- /dev/null +++ b/src/main/resources/db/migration/V40__20261011__restrict_mentorship_cycle_month.sql @@ -0,0 +1,6 @@ +-- Restrict new mentorship cycles to February through November. +-- Existing January and December cycles remain valid historical data. +ALTER TABLE mentorship_cycles + ADD CONSTRAINT valid_cycle_month + CHECK (cycle_month >= 2 AND cycle_month <= 11) + NOT VALID; diff --git a/src/test/java/com/wcc/platform/configuration/GlobalExceptionHandlerTest.java b/src/test/java/com/wcc/platform/configuration/GlobalExceptionHandlerTest.java index f6755ee4..ed971e86 100644 --- a/src/test/java/com/wcc/platform/configuration/GlobalExceptionHandlerTest.java +++ b/src/test/java/com/wcc/platform/configuration/GlobalExceptionHandlerTest.java @@ -15,6 +15,7 @@ import com.wcc.platform.domain.exceptions.ForbiddenException; import com.wcc.platform.domain.platform.mentorship.TechnicalAreaProficiency; import java.util.List; +import java.time.Month; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -160,4 +161,30 @@ void shouldReturnBadRequestWithFallbackMessageForInvalidEnumValue() { assertEquals(BAD_REQUEST, response.getStatusCode()); assertEquals(expectation, response.getBody()); } + + @Test + @DisplayName( + "Given malformed JSON with invalid mentorship cycle month, " + + "when handling, then return the cycle month validation message") + void shouldReturnCycleMonthValidationMessageForInvalidMonth() { + record CycleRequest(Month cycleMonth) {} + + var invalidJson = "{\"cycleMonth\":\"JANUAR\"}"; + var cause = + assertThrows( + InvalidFormatException.class, + () -> new ObjectMapper().readValue(invalidJson, CycleRequest.class)); + var exception = + new HttpMessageNotReadableException( + "JSON parse error", cause, new MockHttpInputMessage(invalidJson.getBytes())); + + var response = + globalExceptionHandler.handleHttpMessageNotReadableException(exception, webRequest); + + var expectation = + new ErrorDetails( + BAD_REQUEST.value(), "Cycle month must be between February and November", DETAILS); + assertEquals(BAD_REQUEST, response.getStatusCode()); + assertEquals(expectation, response.getBody()); + } } diff --git a/src/test/java/com/wcc/platform/controller/MentorshipAdminMatchesControllerTest.java b/src/test/java/com/wcc/platform/controller/MentorshipAdminMatchesControllerTest.java index 3cea8a5b..342ab87a 100644 --- a/src/test/java/com/wcc/platform/controller/MentorshipAdminMatchesControllerTest.java +++ b/src/test/java/com/wcc/platform/controller/MentorshipAdminMatchesControllerTest.java @@ -1,20 +1,28 @@ package com.wcc.platform.controller; +import static org.assertj.core.api.Assertions.assertThat; import static com.wcc.platform.factories.MockMvcRequestFactory.getRequest; import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.wcc.platform.configuration.SecurityConfig; import com.wcc.platform.configuration.TestConfig; +import com.wcc.platform.configuration.security.RequiresRole; +import com.wcc.platform.domain.platform.mentorship.CycleStatus; +import com.wcc.platform.domain.platform.mentorship.MentorshipCycleCreateRequest; import com.wcc.platform.domain.platform.mentorship.MentorshipCycleEntity; +import com.wcc.platform.domain.platform.mentorship.MentorshipType; +import com.wcc.platform.domain.platform.type.RoleType; import com.wcc.platform.repository.MentorshipCycleRepository; import com.wcc.platform.service.AuthService; import com.wcc.platform.service.MenteeWorkflowService; import com.wcc.platform.service.MentorshipCycleService; import com.wcc.platform.service.MentorshipMatchingService; import com.wcc.platform.service.MentorshipRecommendationService; +import java.time.LocalDate; import java.time.Year; import java.util.List; import org.junit.jupiter.api.DisplayName; @@ -74,6 +82,48 @@ void shouldReturn400WhenYearIsNotANumber() throws Exception { .andExpect(status().isBadRequest()); } + @Test + @DisplayName("Given valid cycle request, when creating cycle, then return created cycle") + void shouldCreateCycleAndDelegateToService() throws NoSuchMethodException { + final MentorshipCycleCreateRequest request = request(); + final MentorshipCycleEntity created = + MentorshipCycleEntity.builder().cycleId(9L).status(CycleStatus.DRAFT).build(); + when(cycleService.createCycle(request)).thenReturn(created); + + final var controller = + new MentorshipAdminMatchesController( + matchingService, + cycleRepository, + recommendationService, + workflowService, + cycleService); + final var response = controller.createCycle(request); + + assertThat(response.getStatusCode().value()).isEqualTo(201); + assertThat(response.getBody()).isSameAs(created); + verify(cycleService).createCycle(request); + + final var method = + MentorshipAdminMatchesController.class.getDeclaredMethod( + "createCycle", MentorshipCycleCreateRequest.class); + final var annotation = method.getAnnotation(RequiresRole.class); + assertThat(annotation).isNotNull(); + assertThat(annotation.value()).containsExactly(RoleType.ADMIN, RoleType.MENTORSHIP_ADMIN); + } + + private MentorshipCycleCreateRequest request() { + return new MentorshipCycleCreateRequest( + Year.of(2026), + java.time.Month.OCTOBER, + MentorshipType.AD_HOC, + LocalDate.of(2026, 10, 1), + LocalDate.of(2026, 10, 15), + LocalDate.of(2026, 10, 20), + LocalDate.of(2026, 11, 20), + 5, + "October cycle"); + } + private MentorshipCycleEntity cycleInYear(final Long cycleId, final int year) { return MentorshipCycleEntity.builder().cycleId(cycleId).cycleYear(Year.of(year)).build(); } diff --git a/src/test/java/com/wcc/platform/service/MentorshipCycleCreationTest.java b/src/test/java/com/wcc/platform/service/MentorshipCycleCreationTest.java new file mode 100644 index 00000000..826921c3 --- /dev/null +++ b/src/test/java/com/wcc/platform/service/MentorshipCycleCreationTest.java @@ -0,0 +1,127 @@ +package com.wcc.platform.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; + +import com.wcc.platform.domain.exceptions.DuplicatedException; +import com.wcc.platform.domain.platform.mentorship.CycleStatus; +import com.wcc.platform.domain.platform.mentorship.MentorshipCycleCreateRequest; +import com.wcc.platform.domain.platform.mentorship.MentorshipCycleEntity; +import com.wcc.platform.domain.platform.mentorship.MentorshipType; +import com.wcc.platform.repository.MentorshipCycleRepository; +import java.time.LocalDate; +import java.time.Month; +import java.time.Year; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +class MentorshipCycleCreationTest { + + @Mock private MentorshipCycleRepository cycleRepository; + private MentorshipCycleService cycleService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + cycleService = new MentorshipCycleService(cycleRepository); + } + + @Test + @DisplayName("Given valid cycle request, when creating cycle, then create draft cycle") + void shouldCreateDraftCycleWithDefaultMaxMenteesPerMentor() { + final var request = validCreateRequest(null); + final var createdCycle = cycleWithStatus(9L, CycleStatus.DRAFT); + when(cycleRepository.create(org.mockito.ArgumentMatchers.any(MentorshipCycleEntity.class))) + .thenReturn(createdCycle); + + final var result = cycleService.createCycle(request); + + assertThat(result.getStatus()).isEqualTo(CycleStatus.DRAFT); + verify(cycleRepository) + .create( + org.mockito.ArgumentMatchers.argThat( + cycle -> + cycle.getCycleYear().equals(Year.of(2026)) + && cycle.getCycleMonth().equals(Month.OCTOBER) + && cycle.getMentorshipType() == MentorshipType.AD_HOC + && cycle.getMaxMenteesPerMentor() == 5)); + } + + @Test + @DisplayName("Given duplicate cycle, when creating cycle, then throw conflict exception") + void shouldPropagateDuplicateCycleException() { + when(cycleRepository.create(org.mockito.ArgumentMatchers.any(MentorshipCycleEntity.class))) + .thenThrow(new DuplicatedException("cycle already exists")); + final var request = validCreateRequest(5); + + assertThatThrownBy(() -> cycleService.createCycle(request)) + .isInstanceOf(DuplicatedException.class) + .hasMessage("cycle already exists"); + } + + @Test + @DisplayName( + "Given registration end before registration start, when creating cycle, then reject request") + void shouldRejectInvalidRegistrationDateRange() { + final var request = + new MentorshipCycleCreateRequest( + Year.of(2026), Month.OCTOBER, MentorshipType.AD_HOC, + LocalDate.of(2026, 10, 15), LocalDate.of(2026, 10, 1), + LocalDate.of(2026, 10, 20), null, 5, "October cycle"); + + assertThatThrownBy(() -> cycleService.createCycle(request)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Registration end date"); + verify(cycleRepository, never()).create(org.mockito.ArgumentMatchers.any()); + } + + @Test + @DisplayName( + "Given cycle start before registration start, when creating cycle, then reject request") + void shouldRejectCycleStartBeforeRegistrationStart() { + final var request = + new MentorshipCycleCreateRequest( + Year.of(2026), Month.OCTOBER, MentorshipType.AD_HOC, + LocalDate.of(2026, 10, 15), LocalDate.of(2026, 10, 20), + LocalDate.of(2026, 10, 1), null, 5, "October cycle"); + + assertThatThrownBy(() -> cycleService.createCycle(request)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Cycle start date"); + verify(cycleRepository, never()).create(org.mockito.ArgumentMatchers.any()); + } + + @Test + @DisplayName("Given cycle end before cycle start, when creating cycle, then reject request") + void shouldRejectCycleEndBeforeCycleStart() { + final var request = + new MentorshipCycleCreateRequest( + Year.of(2026), Month.OCTOBER, MentorshipType.AD_HOC, + LocalDate.of(2026, 10, 1), LocalDate.of(2026, 10, 15), + LocalDate.of(2026, 10, 20), LocalDate.of(2026, 10, 19), + 5, "October cycle"); + + assertThatThrownBy(() -> cycleService.createCycle(request)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Cycle end date"); + verify(cycleRepository, never()).create(org.mockito.ArgumentMatchers.any()); + } + + private MentorshipCycleCreateRequest validCreateRequest(final Integer maxMenteesPerMentor) { + return new MentorshipCycleCreateRequest( + Year.of(2026), Month.OCTOBER, MentorshipType.AD_HOC, + LocalDate.of(2026, 10, 1), LocalDate.of(2026, 10, 15), + LocalDate.of(2026, 10, 20), LocalDate.of(2026, 12, 20), + maxMenteesPerMentor, "October cycle"); + } + + private MentorshipCycleEntity cycleWithStatus(final Long cycleId, final CycleStatus status) { + return MentorshipCycleEntity.builder().cycleId(cycleId).status(status).build(); + } +} diff --git a/src/test/java/com/wcc/platform/service/MentorshipCycleQueryTest.java b/src/test/java/com/wcc/platform/service/MentorshipCycleQueryTest.java new file mode 100644 index 00000000..3cd9b21c --- /dev/null +++ b/src/test/java/com/wcc/platform/service/MentorshipCycleQueryTest.java @@ -0,0 +1,62 @@ +package com.wcc.platform.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import com.wcc.platform.domain.platform.mentorship.MentorshipCycleEntity; +import com.wcc.platform.repository.MentorshipCycleRepository; +import java.time.Year; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +class MentorshipCycleQueryTest { + + @Mock private MentorshipCycleRepository cycleRepository; + private MentorshipCycleService cycleService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + cycleService = new MentorshipCycleService(cycleRepository); + } + + @Test + @DisplayName("Given no year, when getting cycles, then return all cycles") + void shouldReturnAllCyclesWhenNoYear() { + final var cycles = List.of(cycleInYear(1L, 2025), cycleInYear(2L, 2026)); + when(cycleRepository.getAll()).thenReturn(cycles); + + final var result = cycleService.getCycles(null); + + assertThat(result).isEqualTo(cycles); + } + + @Test + @DisplayName("Given a year, when getting cycles, then return only cycles in that year") + void shouldReturnCyclesForYear() { + final var cycles = List.of(cycleInYear(2L, 2026)); + when(cycleRepository.findByYear(2026)).thenReturn(cycles); + + final var result = cycleService.getCycles(Year.of(2026)); + + assertThat(result).isEqualTo(cycles); + } + + @Test + @DisplayName("Given a year with no cycles, when getting cycles, then return an empty list") + void shouldReturnEmptyListForYearWithNoCycles() { + when(cycleRepository.findByYear(9999)).thenReturn(List.of()); + + final var result = cycleService.getCycles(Year.of(9999)); + + assertThat(result).isEmpty(); + } + + private MentorshipCycleEntity cycleInYear(final Long cycleId, final int year) { + return MentorshipCycleEntity.builder().cycleId(cycleId).cycleYear(Year.of(year)).build(); + } +} diff --git a/src/test/java/com/wcc/platform/service/MentorshipCycleServiceTest.java b/src/test/java/com/wcc/platform/service/MentorshipCycleStatusTest.java similarity index 76% rename from src/test/java/com/wcc/platform/service/MentorshipCycleServiceTest.java rename to src/test/java/com/wcc/platform/service/MentorshipCycleStatusTest.java index c5df9142..090874f3 100644 --- a/src/test/java/com/wcc/platform/service/MentorshipCycleServiceTest.java +++ b/src/test/java/com/wcc/platform/service/MentorshipCycleStatusTest.java @@ -11,8 +11,6 @@ import com.wcc.platform.domain.platform.mentorship.CycleStatus; import com.wcc.platform.domain.platform.mentorship.MentorshipCycleEntity; import com.wcc.platform.repository.MentorshipCycleRepository; -import java.time.Year; -import java.util.List; import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -20,10 +18,9 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; -class MentorshipCycleServiceTest { +class MentorshipCycleStatusTest { @Mock private MentorshipCycleRepository cycleRepository; - private MentorshipCycleService cycleService; @BeforeEach @@ -53,7 +50,7 @@ void shouldAllowDraftToOpen() { when(cycleRepository.findById(1L)).thenReturn(Optional.of(cycle)); when(cycleRepository.updateStatus(1L, CycleStatus.OPEN)).thenReturn(updated); - final MentorshipCycleEntity result = cycleService.updateStatus(1L, CycleStatus.OPEN); + final var result = cycleService.updateStatus(1L, CycleStatus.OPEN); assertThat(result.getStatus()).isEqualTo(CycleStatus.OPEN); verify(cycleRepository).updateStatus(1L, CycleStatus.OPEN); @@ -67,7 +64,7 @@ void shouldAllowDraftToCancelled() { when(cycleRepository.findById(1L)).thenReturn(Optional.of(cycle)); when(cycleRepository.updateStatus(1L, CycleStatus.CANCELLED)).thenReturn(updated); - final MentorshipCycleEntity result = cycleService.updateStatus(1L, CycleStatus.CANCELLED); + final var result = cycleService.updateStatus(1L, CycleStatus.CANCELLED); assertThat(result.getStatus()).isEqualTo(CycleStatus.CANCELLED); } @@ -80,7 +77,7 @@ void shouldAllowOpenToClosed() { when(cycleRepository.findById(1L)).thenReturn(Optional.of(cycle)); when(cycleRepository.updateStatus(1L, CycleStatus.CLOSED)).thenReturn(updated); - final MentorshipCycleEntity result = cycleService.updateStatus(1L, CycleStatus.CLOSED); + final var result = cycleService.updateStatus(1L, CycleStatus.CLOSED); assertThat(result.getStatus()).isEqualTo(CycleStatus.CLOSED); } @@ -94,7 +91,7 @@ void shouldAllowClosedToInProgress() { when(cycleRepository.findById(1L)).thenReturn(Optional.of(cycle)); when(cycleRepository.updateStatus(1L, CycleStatus.IN_PROGRESS)).thenReturn(updated); - final MentorshipCycleEntity result = cycleService.updateStatus(1L, CycleStatus.IN_PROGRESS); + final var result = cycleService.updateStatus(1L, CycleStatus.IN_PROGRESS); assertThat(result.getStatus()).isEqualTo(CycleStatus.IN_PROGRESS); } @@ -108,14 +105,15 @@ void shouldAllowInProgressToCompleted() { when(cycleRepository.findById(1L)).thenReturn(Optional.of(cycle)); when(cycleRepository.updateStatus(1L, CycleStatus.COMPLETED)).thenReturn(updated); - final MentorshipCycleEntity result = cycleService.updateStatus(1L, CycleStatus.COMPLETED); + final var result = cycleService.updateStatus(1L, CycleStatus.COMPLETED); assertThat(result.getStatus()).isEqualTo(CycleStatus.COMPLETED); } @Test @DisplayName( - "Given cycle in DRAFT status, when transitioning to IN_PROGRESS, then throw InvalidCycleStatusTransitionException") + "Given cycle in DRAFT status, when transitioning to IN_PROGRESS, then throw " + + "InvalidCycleStatusTransitionException") void shouldRejectDraftToInProgress() { final var cycle = cycleWithStatus(1L, CycleStatus.DRAFT); when(cycleRepository.findById(1L)).thenReturn(Optional.of(cycle)); @@ -130,7 +128,8 @@ void shouldRejectDraftToInProgress() { @Test @DisplayName( - "Given cycle in OPEN status, when transitioning to DRAFT, then throw InvalidCycleStatusTransitionException") + "Given cycle in OPEN status, when transitioning to DRAFT, then throw " + + "InvalidCycleStatusTransitionException") void shouldRejectOpenToDraft() { final var cycle = cycleWithStatus(1L, CycleStatus.OPEN); when(cycleRepository.findById(1L)).thenReturn(Optional.of(cycle)); @@ -143,7 +142,8 @@ void shouldRejectOpenToDraft() { @Test @DisplayName( - "Given cycle in COMPLETED status, when transitioning to any status, then throw InvalidCycleStatusTransitionException") + "Given cycle in COMPLETED status, when transitioning to any status, then throw " + + "InvalidCycleStatusTransitionException") void shouldRejectAnyTransitionFromCompleted() { final var cycle = cycleWithStatus(1L, CycleStatus.COMPLETED); when(cycleRepository.findById(1L)).thenReturn(Optional.of(cycle)); @@ -155,7 +155,8 @@ void shouldRejectAnyTransitionFromCompleted() { @Test @DisplayName( - "Given cycle in CANCELLED status, when transitioning to any status, then throw InvalidCycleStatusTransitionException") + "Given cycle in CANCELLED status, when transitioning to any status, then throw " + + "InvalidCycleStatusTransitionException") void shouldRejectAnyTransitionFromCancelled() { final var cycle = cycleWithStatus(1L, CycleStatus.CANCELLED); when(cycleRepository.findById(1L)).thenReturn(Optional.of(cycle)); @@ -167,7 +168,8 @@ void shouldRejectAnyTransitionFromCancelled() { @Test @DisplayName( - "Given cycle in OPEN status, when transitioning to same OPEN status, then throw InvalidCycleStatusTransitionException") + "Given cycle in OPEN status, when transitioning to same OPEN status, then throw " + + "InvalidCycleStatusTransitionException") void shouldRejectSameStatusTransition() { final var cycle = cycleWithStatus(1L, CycleStatus.OPEN); when(cycleRepository.findById(1L)).thenReturn(Optional.of(cycle)); @@ -177,43 +179,7 @@ void shouldRejectSameStatusTransition() { .hasMessageContaining("OPEN"); } - @Test - @DisplayName("Given no year, when getting cycles, then return all cycles") - void shouldReturnAllCyclesWhenNoYear() { - final var cycles = List.of(cycleInYear(1L, 2025), cycleInYear(2L, 2026)); - when(cycleRepository.getAll()).thenReturn(cycles); - - final List result = cycleService.getCycles(null); - - assertThat(result).isEqualTo(cycles); - } - - @Test - @DisplayName("Given a year, when getting cycles, then return only cycles in that year") - void shouldReturnCyclesForYear() { - final var cycles = List.of(cycleInYear(2L, 2026)); - when(cycleRepository.findByYear(2026)).thenReturn(cycles); - - final List result = cycleService.getCycles(Year.of(2026)); - - assertThat(result).isEqualTo(cycles); - } - - @Test - @DisplayName("Given a year with no cycles, when getting cycles, then return an empty list") - void shouldReturnEmptyListForYearWithNoCycles() { - when(cycleRepository.findByYear(9999)).thenReturn(List.of()); - - final List result = cycleService.getCycles(Year.of(9999)); - - assertThat(result).isEmpty(); - } - private MentorshipCycleEntity cycleWithStatus(final Long cycleId, final CycleStatus status) { return MentorshipCycleEntity.builder().cycleId(cycleId).status(status).build(); } - - private MentorshipCycleEntity cycleInYear(final Long cycleId, final int year) { - return MentorshipCycleEntity.builder().cycleId(cycleId).cycleYear(Year.of(year)).build(); - } } diff --git a/src/testInt/java/com/wcc/platform/repository/postgres/PostgresMenteeApplicationRepositoryIntegrationTest.java b/src/testInt/java/com/wcc/platform/repository/postgres/PostgresMenteeApplicationRepositoryIntegrationTest.java index 23346346..d1b47a01 100644 --- a/src/testInt/java/com/wcc/platform/repository/postgres/PostgresMenteeApplicationRepositoryIntegrationTest.java +++ b/src/testInt/java/com/wcc/platform/repository/postgres/PostgresMenteeApplicationRepositoryIntegrationTest.java @@ -54,7 +54,7 @@ void setUp() { MentorshipCycleEntity.builder() .cycleYear(Year.of(2026)) .mentorshipType(MentorshipType.LONG_TERM) - .cycleMonth(Month.JANUARY) + .cycleMonth(Month.FEBRUARY) .registrationStartDate(LocalDate.now().minusDays(1)) .registrationEndDate(LocalDate.now().plusDays(10)) .cycleStartDate(LocalDate.now().plusDays(15)) diff --git a/src/testInt/java/com/wcc/platform/service/MenteeServiceIntegrationTest.java b/src/testInt/java/com/wcc/platform/service/MenteeServiceIntegrationTest.java index f8d08bb8..9928209c 100644 --- a/src/testInt/java/com/wcc/platform/service/MenteeServiceIntegrationTest.java +++ b/src/testInt/java/com/wcc/platform/service/MenteeServiceIntegrationTest.java @@ -349,14 +349,14 @@ private void createAdHocCycle() { MentorshipCycleEntity.builder() .cycleYear(TEST_YEAR_2028) .mentorshipType(MentorshipType.AD_HOC) - .cycleMonth(Month.DECEMBER) + .cycleMonth(Month.NOVEMBER) .registrationStartDate(LocalDate.now().minusDays(5)) .registrationEndDate(LocalDate.now().plusDays(5)) .cycleStartDate(LocalDate.now().plusDays(1)) .cycleEndDate(LocalDate.now().plusDays(30)) .status(CycleStatus.OPEN) .maxMenteesPerMentor(MAX_AD_HOC_MENTEES) - .description("Test AD_HOC cycle for December 2028") + .description("Test AD_HOC cycle for November 2028") .build(); final var savedCycle = cycleRepository.create(adHocCycle); diff --git a/src/testInt/java/com/wcc/platform/service/MentorshipCycleEndpointIntegrationTest.java b/src/testInt/java/com/wcc/platform/service/MentorshipCycleEndpointIntegrationTest.java new file mode 100644 index 00000000..2f28eec0 --- /dev/null +++ b/src/testInt/java/com/wcc/platform/service/MentorshipCycleEndpointIntegrationTest.java @@ -0,0 +1,166 @@ +package com.wcc.platform.service; + +import static org.assertj.core.api.Assertions.assertThat; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.time.Month; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; + +/** Integration tests for mentorship cycle REST endpoints. */ +@ActiveProfiles("test") +class MentorshipCycleEndpointIntegrationTest extends MentorshipCycleIntegrationTestSupport { + + @Autowired private ObjectMapper objectMapper; + + @Test + @DisplayName("Given a past year, when getting cycles, then it should throw ConstraintViolationException") + void shouldRejectPastYear() { + org.assertj.core.api.Assertions.assertThatThrownBy(() -> cycleService.getCycles(java.time.Year.of(2025))) + .isInstanceOf(jakarta.validation.ConstraintViolationException.class); + } + + @Test + @DisplayName("Given valid cycle request, when posting it, then return 201 with draft cycle") + void shouldCreateCycleThroughPostEndpoint() throws Exception { + final ResponseEntity response = + restTemplate.exchange(url(), HttpMethod.POST, requestEntity(validRequest()), String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); + assertThat(response.getBody()).isNotNull(); + final var body = objectMapper.readTree(response.getBody()); + assertThat(body.get("status").asText()).isEqualTo("DRAFT"); + assertThat(body.get("cycleYear").asInt()).isEqualTo(TEST_YEAR); + assertThat(body.get("mentorshipType").asText()).isEqualTo("Ad-Hoc"); + assertThat(body.get("cycleMonth").asText()).isEqualTo("NOVEMBER"); + } + + @Test + @DisplayName("Given existing cycle, when posting duplicate, then return 409") + void shouldReturnConflictForDuplicateCycle() { + final HttpEntity entity = requestEntity(validRequest()); + restTemplate.exchange(url(), HttpMethod.POST, entity, String.class); + + final ResponseEntity response = + restTemplate.exchange(url(), HttpMethod.POST, entity, String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); + assertThat(response.getBody()).contains("already exists"); + } + + @Test + @DisplayName("Given missing required field, when posting cycle, then return 400") + void shouldReturnBadRequestForMissingRequiredField() { + final String body = + """ + { + "cycleMonth": "DECEMBER", + "mentorshipType": "AD_HOC", + "registrationStartDate": "2099-12-01", + "registrationEndDate": "2099-12-15", + "cycleStartDate": "2099-12-20" + } + """; + + final ResponseEntity response = + restTemplate.exchange(url(), HttpMethod.POST, requestEntity(body), String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(response.getBody()).contains("cycleYear"); + } + + @Test + @DisplayName("Given invalid cycle month, when posting cycle, then return 400") + void shouldReturnBadRequestForInvalidCycleMonth() { + final CycleRequest request = + new CycleRequest( + TEST_YEAR, Month.JANUARY, "AD_HOC", "2099-11-01", "2099-11-15", + "2099-11-20", "2100-02-20", 5, "January ad-hoc cycle"); + + final ResponseEntity response = + restTemplate.exchange(url(), HttpMethod.POST, requestEntity(request), String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(response.getBody()).contains("cycleMonth"); + } + + @Test + @DisplayName("Given invalid mentee cap, when posting cycle, then return 400") + void shouldReturnBadRequestForInvalidMaxMenteesPerMentor() { + final CycleRequest validRequest = validRequest(); + final CycleRequest request = + new CycleRequest( + validRequest.cycleYear(), validRequest.cycleMonth(), validRequest.mentorshipType(), + validRequest.registrationStartDate(), validRequest.registrationEndDate(), + validRequest.cycleStartDate(), validRequest.cycleEndDate(), 0, + validRequest.description()); + + final ResponseEntity response = + restTemplate.exchange(url(), HttpMethod.POST, requestEntity(request), String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(response.getBody()).contains("maxMenteesPerMentor"); + } + + @Test + @DisplayName("Given invalid date order, when posting cycle, then return 400") + void shouldReturnBadRequestForInvalidDateOrder() { + final CycleRequest validRequest = validRequest(); + final CycleRequest request = + new CycleRequest( + validRequest.cycleYear(), validRequest.cycleMonth(), validRequest.mentorshipType(), + validRequest.registrationStartDate(), "2099-10-30", validRequest.cycleStartDate(), + validRequest.cycleEndDate(), validRequest.maxMenteesPerMentor(), + validRequest.description()); + + final ResponseEntity response = + restTemplate.exchange(url(), HttpMethod.POST, requestEntity(request), String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(response.getBody()).contains("Registration end date"); + } + + private String url() { + return "http://localhost:" + port + CYCLES_PATH; + } + + private HttpEntity requestEntity(final CycleRequest request) { + return new HttpEntity<>(request, headers()); + } + + private HttpEntity requestEntity(final String body) { + return new HttpEntity<>(body, headers()); + } + + private HttpHeaders headers() { + final HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.setBearerAuth(adminToken); + headers.add("X-API-KEY", "test-api-key"); + return headers; + } + + private CycleRequest validRequest() { + return new CycleRequest( + TEST_YEAR, Month.NOVEMBER, "AD_HOC", "2099-11-01", "2099-11-15", + "2099-11-20", "2100-02-20", 5, "November ad-hoc cycle"); + } + + private record CycleRequest( + int cycleYear, + Month cycleMonth, + String mentorshipType, + String registrationStartDate, + String registrationEndDate, + String cycleStartDate, + String cycleEndDate, + int maxMenteesPerMentor, + String description) {} +} diff --git a/src/testInt/java/com/wcc/platform/service/MentorshipCycleIntegrationTestSupport.java b/src/testInt/java/com/wcc/platform/service/MentorshipCycleIntegrationTestSupport.java new file mode 100644 index 00000000..47d46f83 --- /dev/null +++ b/src/testInt/java/com/wcc/platform/service/MentorshipCycleIntegrationTestSupport.java @@ -0,0 +1,129 @@ +package com.wcc.platform.service; + +import com.wcc.platform.domain.auth.UserAccount; +import com.wcc.platform.domain.platform.mentorship.CycleStatus; +import com.wcc.platform.domain.platform.mentorship.MentorshipCycleEntity; +import com.wcc.platform.domain.platform.mentorship.MentorshipType; +import com.wcc.platform.domain.platform.member.Member; +import com.wcc.platform.domain.platform.type.RoleType; +import com.wcc.platform.repository.MemberRepository; +import com.wcc.platform.repository.MentorshipCycleRepository; +import com.wcc.platform.repository.UserAccountRepository; +import com.wcc.platform.repository.postgres.DefaultDatabaseSetup; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import java.time.LocalDate; +import java.time.Month; +import java.time.Year; +import java.util.List; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** Shared PostgreSQL setup for mentorship cycle integration tests. */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +abstract class MentorshipCycleIntegrationTestSupport extends DefaultDatabaseSetup { + + protected static final int TEST_YEAR = 2099; + protected static final String ADMIN_EMAIL = "mentorship-cycle-admin@wcc.com"; + protected static final String ADMIN_PASSWORD = "password"; + protected static final String CYCLES_PATH = "/api/platform/v1/admin/mentorship/cycles"; + protected static final String LOGIN_PATH = "/api/auth/login"; + + @LocalServerPort protected int port; + + @Autowired protected TestRestTemplate restTemplate; + @Autowired protected MentorshipCycleRepository cycleRepository; + @Autowired protected MentorshipCycleService cycleService; + @Autowired protected MemberRepository memberRepository; + @Autowired protected UserAccountRepository userAccountRepository; + @Autowired protected PasswordEncoder passwordEncoder; + + protected String adminToken; + + @BeforeAll + void setUpAdmin() { + final Member member = + memberRepository + .findByEmail(ADMIN_EMAIL) + .orElseGet( + () -> + memberRepository.create( + Member.builder() + .fullName("Mentorship Cycle Admin") + .position("Administrator") + .email(ADMIN_EMAIL) + .slackDisplayName("mentorship-cycle-admin") + .build())); + userAccountRepository + .findByEmail(ADMIN_EMAIL) + .orElseGet( + () -> + userAccountRepository.create( + new UserAccount( + null, + member.getId(), + ADMIN_EMAIL, + passwordEncoder.encode(ADMIN_PASSWORD), + List.of(RoleType.ADMIN), + true))); + adminToken = loginAsAdmin(); + } + + @BeforeEach + void setUpCycle() { + cycleRepository + .findByYearAndTypeAndMonth( + Year.of(TEST_YEAR), MentorshipType.AD_HOC, Month.NOVEMBER) + .ifPresent(cycle -> cycleRepository.deleteById(cycle.getCycleId())); + cycleRepository + .findByYearAndType(Year.of(2026), MentorshipType.LONG_TERM) + .ifPresent(cycle -> cycleRepository.deleteById(cycle.getCycleId())); + cycleRepository.create( + MentorshipCycleEntity.builder() + .cycleYear(Year.of(2026)) + .mentorshipType(MentorshipType.LONG_TERM) + .cycleMonth(Month.FEBRUARY) + .registrationStartDate(LocalDate.now().minusDays(1)) + .registrationEndDate(LocalDate.now().plusDays(10)) + .cycleStartDate(LocalDate.now().plusDays(15)) + .status(CycleStatus.OPEN) + .maxMenteesPerMentor(3) + .description("Test Cycle") + .build()); + } + + @AfterAll + void tearDownAdmin() { + userAccountRepository + .findByEmail(ADMIN_EMAIL) + .ifPresent(user -> userAccountRepository.deleteById(user.getId())); + memberRepository + .findByEmail(ADMIN_EMAIL) + .ifPresent(member -> memberRepository.deleteById(member.getId())); + } + + protected String loginAsAdmin() { + final HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + final ResponseEntity response = + restTemplate.postForEntity( + "http://localhost:" + port + LOGIN_PATH, + new HttpEntity<>(new LoginRequest(ADMIN_EMAIL, ADMIN_PASSWORD), headers), + LoginResponse.class); + return response.getBody().token(); + } + + private record LoginRequest(String email, String password) {} + + @JsonIgnoreProperties(ignoreUnknown = true) + private record LoginResponse(String token) {} +} diff --git a/src/testInt/java/com/wcc/platform/service/MentorshipCycleIntegrationTest.java b/src/testInt/java/com/wcc/platform/service/MentorshipCycleRepositoryIntegrationTest.java similarity index 74% rename from src/testInt/java/com/wcc/platform/service/MentorshipCycleIntegrationTest.java rename to src/testInt/java/com/wcc/platform/service/MentorshipCycleRepositoryIntegrationTest.java index 37fcdc7a..8d62dc94 100644 --- a/src/testInt/java/com/wcc/platform/service/MentorshipCycleIntegrationTest.java +++ b/src/testInt/java/com/wcc/platform/service/MentorshipCycleRepositoryIntegrationTest.java @@ -1,54 +1,26 @@ package com.wcc.platform.service; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.wcc.platform.domain.platform.mentorship.CycleStatus; import com.wcc.platform.domain.platform.mentorship.MentorshipCycleEntity; -import com.wcc.platform.domain.platform.mentorship.MentorshipType; import com.wcc.platform.repository.MentorshipCycleRepository; -import com.wcc.platform.repository.postgres.DefaultDatabaseSetup; -import jakarta.validation.ConstraintViolationException; -import java.time.LocalDate; -import java.time.Month; import java.time.Year; import java.util.List; import java.util.Optional; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ActiveProfiles; /** * Integration tests for MentorshipCycleRepository with PostgreSQL. Tests cycle queries and * management operations. */ -class MentorshipCycleIntegrationTest extends DefaultDatabaseSetup { +@ActiveProfiles("test") +class MentorshipCycleRepositoryIntegrationTest extends MentorshipCycleIntegrationTestSupport { @Autowired private MentorshipCycleRepository cycleRepository; - @Autowired private MentorshipCycleService cycleService; - - @BeforeEach - void setUp() { - // Clean up before starting - cycleRepository - .findByYearAndType(Year.of(2026), MentorshipType.LONG_TERM) - .ifPresent(c -> cycleRepository.deleteById(c.getCycleId())); - - // Setup cycle - cycleRepository.create( - MentorshipCycleEntity.builder() - .cycleYear(Year.of(2026)) - .mentorshipType(MentorshipType.LONG_TERM) - .cycleMonth(Month.JANUARY) - .registrationStartDate(LocalDate.now().minusDays(1)) - .registrationEndDate(LocalDate.now().plusDays(10)) - .cycleStartDate(LocalDate.now().plusDays(15)) - .status(CycleStatus.OPEN) - .maxMenteesPerMentor(3) - .description("Test Cycle") - .build()); - } @Test @DisplayName( @@ -151,12 +123,4 @@ void shouldReturnEmptyForYearWithNoCycles() { assertThat(cycles).isEmpty(); } - - @Test - @DisplayName( - "Given a past year, when getting cycles, then it should throw ConstraintViolationException") - void shouldRejectPastYear() { - assertThatThrownBy(() -> cycleService.getCycles(Year.of(2025))) - .isInstanceOf(ConstraintViolationException.class); - } }