diff --git a/pom.xml b/pom.xml index 4d23ebe..c269b1f 100644 --- a/pom.xml +++ b/pom.xml @@ -209,6 +209,10 @@ loki-logback-appender 1.5.2 --> + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + diff --git a/src/main/java/org/patinanetwork/patchats/api/member/MemberController.java b/src/main/java/org/patinanetwork/patchats/api/member/MemberController.java index 1d615d6..7c0ccc7 100644 --- a/src/main/java/org/patinanetwork/patchats/api/member/MemberController.java +++ b/src/main/java/org/patinanetwork/patchats/api/member/MemberController.java @@ -2,11 +2,15 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import java.util.UUID; import lombok.RequiredArgsConstructor; import org.patinanetwork.patchats.api.member.dto.CreateMemberRequest; import org.patinanetwork.patchats.api.member.dto.MemberDto; +import org.patinanetwork.patchats.api.member.dto.UpdateMemberRequest; import org.patinanetwork.patchats.common.dto.ApiResponder; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -26,13 +30,12 @@ public ResponseEntity> createMember(@Valid @RequestBody return ResponseEntity.ok(ApiResponder.success("Member created successfully", response)); } - // TODO: Implement these endpoints after createMember is fully functional and tested - // @PatchMapping("/{id}") - // public ResponseEntity> updateMember( - // @Valid @RequestBody final UpdateMemberRequest request, @PathVariable final UUID id) { - // final MemberDto response = memberService.updateMember(request, id); - // return ResponseEntity.ok(ApiResponder.success("Member updated successfully", response)); - // } + @PatchMapping("/{id}") + public ResponseEntity> updateMember( + @Valid @RequestBody final UpdateMemberRequest request, @PathVariable final UUID id) { + final MemberDto response = memberService.updateMember(request, id); + return ResponseEntity.ok(ApiResponder.success("Member updated successfully", response)); + } // @GetMapping("/{id}") // public ResponseEntity> getMember(@PathVariable final UUID id) { diff --git a/src/main/java/org/patinanetwork/patchats/api/member/MemberService.java b/src/main/java/org/patinanetwork/patchats/api/member/MemberService.java index 169b9ad..29c7c2e 100644 --- a/src/main/java/org/patinanetwork/patchats/api/member/MemberService.java +++ b/src/main/java/org/patinanetwork/patchats/api/member/MemberService.java @@ -1,7 +1,9 @@ package org.patinanetwork.patchats.api.member; import java.util.List; +import java.util.Optional; import java.util.UUID; +import java.util.function.Consumer; import lombok.RequiredArgsConstructor; import org.patinanetwork.patchats.api.member.db.models.Member; import org.patinanetwork.patchats.api.member.db.repos.MemberRepo; @@ -10,6 +12,7 @@ import org.patinanetwork.patchats.api.member.dto.UpdateMemberRequest; import org.patinanetwork.patchats.common.web.exception.MemberDuplicateException; import org.patinanetwork.patchats.common.web.exception.MemberNotFoundException; +import org.patinanetwork.patchats.common.web.exception.ValidationException; import org.springframework.stereotype.Service; @Service @@ -48,40 +51,53 @@ public List getMembers() { public MemberDto updateMember(UpdateMemberRequest request, UUID id) { Member member = memberRepo.getMemberById(id).orElseThrow(() -> new MemberNotFoundException(id)); - if (request.firstName() != null) { - member.setFirstName(request.firstName()); - } - if (request.lastName() != null) { - member.setLastName(request.lastName()); - } - if (request.email() != null) { - member.setEmail(request.email()); - } - if (request.linkedInUrl() != null) { - member.setLinkedInUrl(request.linkedInUrl()); - } - if (request.introduction() != null) { - member.setIntroduction(request.introduction()); - } - if (request.matchPref() != null) { - member.setMatchPref(request.matchPref()); - } - if (request.industryPref() != null) { - member.setIndustryPref(request.industryPref()); - } - if (request.rolePref() != null) { - member.setRolePref(request.rolePref()); - } - if (request.topics() != null) { - member.setTopics(request.topics()); - } - if (request.extraNotes() != null) { - member.setExtraNotes(request.extraNotes()); - } + validateAndUpdateRequired(member, request); + updateOptional(member, request); + Member updatedMember = memberRepo.updateMember(member).orElseThrow(() -> new MemberNotFoundException(id)); return MemberDto.from(updatedMember); } + private void validateAndUpdateRequired(Member member, UpdateMemberRequest request) { + validateAndUpdate(request.firstName(), member::setFirstName, "firstName"); + validateAndUpdate(request.lastName(), member::setLastName, "lastName"); + validateAndUpdateEmail(member, request); + validateAndUpdate(request.introduction(), member::setIntroduction, "introduction"); + } + + private void validateAndUpdate(Optional field, Consumer setter, String fieldName) { + if (field.isPresent()) { + String value = field.get(); + if (value.isBlank()) { + throw new ValidationException(fieldName + " cannot be empty"); + } + setter.accept(value); + } + } + + private void validateAndUpdateEmail(Member member, UpdateMemberRequest request) { + if (request.email().isPresent()) { + String email = request.email().get(); + if (email.isBlank()) { + throw new ValidationException("email cannot be empty"); + } + if (!email.equals(member.getEmail()) + && memberRepo.getMemberByEmail(email).isPresent()) { + throw new MemberDuplicateException(email); + } + member.setEmail(email); + } + } + + private void updateOptional(Member member, UpdateMemberRequest request) { + request.linkedInUrl().ifPresent(member::setLinkedInUrl); + request.matchPref().ifPresent(member::setMatchPref); + request.industryPref().ifPresent(member::setIndustryPref); + request.rolePref().ifPresent(member::setRolePref); + request.topics().ifPresent(member::setTopics); + request.extraNotes().ifPresent(member::setExtraNotes); + } + // TODO: Implement these methods after createMember and updateMember is fully functional and tested // public MemberDto getMemberById(UUID id) { // return memberRepo diff --git a/src/main/java/org/patinanetwork/patchats/api/member/db/repos/MemberSqlRepo.java b/src/main/java/org/patinanetwork/patchats/api/member/db/repos/MemberSqlRepo.java index ccc65d4..cabf9a8 100644 --- a/src/main/java/org/patinanetwork/patchats/api/member/db/repos/MemberSqlRepo.java +++ b/src/main/java/org/patinanetwork/patchats/api/member/db/repos/MemberSqlRepo.java @@ -37,6 +37,22 @@ private Member parseResultSetToMember(final ResultSet rs) throws SQLException { .build(); } + private JdbcClient.StatementSpec bindMemberParams(JdbcClient.StatementSpec spec, Member member) { + return spec.param("id", member.getId()) + .param("first_name", member.getFirstName()) + .param("last_name", member.getLastName()) + .param("email", member.getEmail()) + .param("linked_in_url", member.getLinkedInUrl()) + .param("introduction", member.getIntroduction()) + .param("referral_source", member.getReferralSource()) + .param("active", member.isActive()) + .param("match_pref", member.getMatchPref()) + .param("industry_pref", member.getIndustryPref()) + .param("role_pref", member.getRolePref()) + .param("topics", member.getTopics()) + .param("extra_notes", member.getExtraNotes()); + } + @Override public Member createMember(Member member) { String sql = """ @@ -73,20 +89,7 @@ public Member createMember(Member member) { RETURNING * """; - return jdbc.sql(sql) - .param("id", member.getId()) - .param("first_name", member.getFirstName()) - .param("last_name", member.getLastName()) - .param("email", member.getEmail()) - .param("linked_in_url", member.getLinkedInUrl()) - .param("introduction", member.getIntroduction()) - .param("referral_source", member.getReferralSource()) - .param("active", member.isActive()) - .param("match_pref", member.getMatchPref()) - .param("industry_pref", member.getIndustryPref()) - .param("role_pref", member.getRolePref()) - .param("topics", member.getTopics()) - .param("extra_notes", member.getExtraNotes()) + return bindMemberParams(jdbc.sql(sql), member) .query((rs, rowNum) -> parseResultSetToMember(rs)) .single(); } @@ -99,12 +102,36 @@ public List getMembers() { @Override public Optional updateMember(Member member) { - throw new UnsupportedOperationException("Not implemented yet"); + String sql = """ + UPDATE "members" SET + "first_name" = :first_name, + "last_name" = :last_name, + "email" = :email, + "linked_in_url" = :linked_in_url, + "introduction" = :introduction, + "referral_source" = :referral_source, + "active" = :active, + "match_pref" = :match_pref, + "industry_pref" = :industry_pref, + "role_pref" = :role_pref, + "topics" = :topics, + "extra_notes" = :extra_notes, + "updated_at" = NOW() + WHERE "id" = :id + RETURNING * + """; + return bindMemberParams(jdbc.sql(sql), member) + .query((rs, rowNum) -> parseResultSetToMember(rs)) + .optional(); } @Override public Optional getMemberById(UUID id) { - throw new UnsupportedOperationException("Not implemented yet"); + String sql = "SELECT * FROM members WHERE id = :id"; + return jdbc.sql(sql) + .param("id", id) + .query((rs, rowNum) -> parseResultSetToMember(rs)) + .optional(); } @Override diff --git a/src/main/java/org/patinanetwork/patchats/api/member/dto/UpdateMemberRequest.java b/src/main/java/org/patinanetwork/patchats/api/member/dto/UpdateMemberRequest.java index 290ec98..83d1eaa 100644 --- a/src/main/java/org/patinanetwork/patchats/api/member/dto/UpdateMemberRequest.java +++ b/src/main/java/org/patinanetwork/patchats/api/member/dto/UpdateMemberRequest.java @@ -1,16 +1,16 @@ package org.patinanetwork.patchats.api.member.dto; import jakarta.validation.constraints.Email; +import java.util.Optional; public record UpdateMemberRequest( - String firstName, - String lastName, - // TODO: Changes to email require verification after authentication is implemented - @Email String email, - String linkedInUrl, - String introduction, - String matchPref, - String industryPref, - String rolePref, - String topics, - String extraNotes) {} + Optional firstName, + Optional lastName, + Optional<@Email String> email, + Optional linkedInUrl, + Optional introduction, + Optional matchPref, + Optional industryPref, + Optional rolePref, + Optional topics, + Optional extraNotes) {} diff --git a/src/main/java/org/patinanetwork/patchats/common/web/ApiExceptionHandler.java b/src/main/java/org/patinanetwork/patchats/common/web/ApiExceptionHandler.java index 7501ad8..e8b60c7 100644 --- a/src/main/java/org/patinanetwork/patchats/common/web/ApiExceptionHandler.java +++ b/src/main/java/org/patinanetwork/patchats/common/web/ApiExceptionHandler.java @@ -4,6 +4,7 @@ import org.patinanetwork.patchats.common.dto.ApiResponder; import org.patinanetwork.patchats.common.web.exception.MemberDuplicateException; import org.patinanetwork.patchats.common.web.exception.MemberNotFoundException; +import org.patinanetwork.patchats.common.web.exception.ValidationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.FieldError; @@ -34,6 +35,11 @@ public ResponseEntity> handleMemberDuplicate(final MemberDupl return ResponseEntity.status(HttpStatus.CONFLICT).body(ApiResponder.failure(ex.getMessage())); } + @ExceptionHandler(ValidationException.class) + public ResponseEntity> handleValidation(ValidationException ex) { + return ResponseEntity.badRequest().body(ApiResponder.failure(ex.getMessage())); + } + private String formatError(final FieldError error) { return error.getField() + " " + error.getDefaultMessage(); } diff --git a/src/main/java/org/patinanetwork/patchats/common/web/exception/ValidationException.java b/src/main/java/org/patinanetwork/patchats/common/web/exception/ValidationException.java new file mode 100644 index 0000000..4e97d6a --- /dev/null +++ b/src/main/java/org/patinanetwork/patchats/common/web/exception/ValidationException.java @@ -0,0 +1,11 @@ +package org.patinanetwork.patchats.common.web.exception; + +public class ValidationException extends RuntimeException { + public ValidationException(String message) { + super(message); + } + + public ValidationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/test/java/org/patinanetwork/patchats/api/member/MemberControllerTest.java b/src/test/java/org/patinanetwork/patchats/api/member/MemberControllerTest.java index 8532e87..05d20a3 100644 --- a/src/test/java/org/patinanetwork/patchats/api/member/MemberControllerTest.java +++ b/src/test/java/org/patinanetwork/patchats/api/member/MemberControllerTest.java @@ -1,17 +1,26 @@ package org.patinanetwork.patchats.api.member; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.time.OffsetDateTime; +import java.util.Optional; import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.patinanetwork.patchats.api.member.dto.CreateMemberRequest; import org.patinanetwork.patchats.api.member.dto.MemberDto; +import org.patinanetwork.patchats.api.member.dto.UpdateMemberRequest; import org.patinanetwork.patchats.common.web.ApiExceptionHandler; import org.patinanetwork.patchats.common.web.exception.MemberDuplicateException; +import org.patinanetwork.patchats.common.web.exception.MemberNotFoundException; +import org.patinanetwork.patchats.common.web.exception.ValidationException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -31,8 +40,15 @@ class MemberControllerTest { @MockitoBean private MemberService memberService; + private final ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + void setUp() { + objectMapper.registerModule(new com.fasterxml.jackson.datatype.jdk8.Jdk8Module()); + } + @Test - void createMemberReturnsOkAndMemberDto() throws Exception { + void createMember_returnsOkAndMemberDto() throws Exception { final CreateMemberRequest request = new CreateMemberRequest( "John", "Doe", @@ -106,4 +122,214 @@ void createMemberReturnsConflictOnDuplicateEmail() throws Exception { .andExpect(status().isConflict()) .andExpect(jsonPath("$.success").value(false)); } + + @Test + void updateMember_successWithValidRequest() throws Exception { + final UUID id = UUID.randomUUID(); + final UpdateMemberRequest request = new UpdateMemberRequest( + Optional.of("UpdatedFirstName"), + Optional.of("UpdatedLastName"), + Optional.of("updated@example.com"), + Optional.of("https://linkedin.com/in/updated"), + Optional.of("Updated intro"), + Optional.of("Mentor"), + Optional.of("Tech"), + Optional.of("Engineer"), + Optional.of("AI,ML"), + Optional.of("Notes")); + + final MemberDto response = MemberDto.builder() + .id(id) + .firstName("UpdatedFirstName") + .lastName("UpdatedLastName") + .email("updated@example.com") + .linkedInUrl("https://linkedin.com/in/updated") + .introduction("Updated intro") + .referralSource("Referral") + .active(true) + .matchPref("Mentor") + .industryPref("Tech") + .rolePref("Engineer") + .topics("AI,ML") + .extraNotes("Notes") + .createdAt(OffsetDateTime.now()) + .updatedAt(OffsetDateTime.now()) + .build(); + + when(memberService.updateMember(any(UpdateMemberRequest.class), eq(id))).thenReturn(response); + + mockMvc.perform(patch("/api/members/{id}", id) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.message").value("Member updated successfully")) + .andExpect(jsonPath("$.payload.id").value(id.toString())) + .andExpect(jsonPath("$.payload.firstName").value("UpdatedFirstName")) + .andExpect(jsonPath("$.payload.lastName").value("UpdatedLastName")) + .andExpect(jsonPath("$.payload.email").value("updated@example.com")); + } + + @Test + void updateMember_successWithPartialUpdate() throws Exception { + final UUID id = UUID.randomUUID(); + final UpdateMemberRequest request = new UpdateMemberRequest( + Optional.of("UpdatedFirstName"), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty()); + + final MemberDto response = MemberDto.builder() + .id(id) + .firstName("UpdatedFirstName") + .lastName("OldLastName") + .email("old@example.com") + .linkedInUrl("https://linkedin.com/in/old") + .introduction("Old intro") + .referralSource("Referral") + .active(true) + .matchPref("Mentor") + .industryPref("Finance") + .rolePref("Analyst") + .topics("Economics") + .extraNotes("Old notes") + .createdAt(OffsetDateTime.now()) + .updatedAt(OffsetDateTime.now()) + .build(); + + when(memberService.updateMember(any(UpdateMemberRequest.class), eq(id))).thenReturn(response); + + mockMvc.perform(patch("/api/members/{id}", id) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.payload.firstName").value("UpdatedFirstName")); + } + + @Test + void updateMember_badRequestWhenBlankRequiredField() throws Exception { + final UUID id = UUID.randomUUID(); + final UpdateMemberRequest request = new UpdateMemberRequest( + Optional.of(""), + Optional.of("UpdatedLastName"), + Optional.of("updated@example.com"), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty()); + + when(memberService.updateMember(any(), any())).thenThrow(new ValidationException("firstName cannot be empty")); + + mockMvc.perform(patch("/api/members/{id}", id) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("firstName cannot be empty")); + } + + @Test + void updateMember_notFoundWhenMemberDoesNotExist() throws Exception { + final UUID id = UUID.randomUUID(); + final UpdateMemberRequest request = new UpdateMemberRequest( + Optional.of("UpdatedFirstName"), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty()); + + when(memberService.updateMember(any(UpdateMemberRequest.class), eq(id))) + .thenThrow(new MemberNotFoundException(id)); + + mockMvc.perform(patch("/api/members/{id}", id) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.success").value(false)); + } + + @Test + void updateMember_conflictWhenEmailIsDuplicate() throws Exception { + final UUID id = UUID.randomUUID(); + final UpdateMemberRequest request = new UpdateMemberRequest( + Optional.empty(), + Optional.empty(), + Optional.of("existing@example.com"), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty()); + + when(memberService.updateMember(any(UpdateMemberRequest.class), eq(id))) + .thenThrow(new MemberDuplicateException("existing@example.com")); + + mockMvc.perform(patch("/api/members/{id}", id) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.success").value(false)); + } + + @Test + void updateMember_badRequestWhenValidationFails() throws Exception { + final UUID id = UUID.randomUUID(); + final UpdateMemberRequest request = new UpdateMemberRequest( + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty()); + + when(memberService.updateMember(any(UpdateMemberRequest.class), eq(id))) + .thenThrow(new ValidationException("firstName cannot be empty")); + + mockMvc.perform(patch("/api/members/{id}", id) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.message").value("firstName cannot be empty")); + } + + @Test + void updateMember_badRequestWhenInvalidUuid() throws Exception { + final UpdateMemberRequest request = new UpdateMemberRequest( + Optional.of("UpdatedFirstName"), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty()); + + mockMvc.perform(patch("/api/members/{id}", "invalid-uuid") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()); + } } diff --git a/src/test/java/org/patinanetwork/patchats/api/member/MemberServiceTest.java b/src/test/java/org/patinanetwork/patchats/api/member/MemberServiceTest.java index 8bf3318..e8f35d3 100644 --- a/src/test/java/org/patinanetwork/patchats/api/member/MemberServiceTest.java +++ b/src/test/java/org/patinanetwork/patchats/api/member/MemberServiceTest.java @@ -116,16 +116,16 @@ void createMemberThrowsExceptionWhenEmailAlreadyExists() { void updateMember_throwsExceptionWhenMemberNotFound() { final UUID id = UUID.randomUUID(); final UpdateMemberRequest request = new UpdateMemberRequest( - "UpdatedFirstName", - "UpdatedLastName", - "updated@example.com", - "https://linkedin.com/in/updated", - "Updated intro", - "Mentor - I am looking for guidance from someone with more experience", - "Technology", - "Software Engineer", - "AI,ML", - "Notes"); + Optional.of("UpdatedFirstName"), + Optional.of("UpdatedLastName"), + Optional.of("updated@example.com"), + Optional.of("https://linkedin.com/in/updated"), + Optional.of("Updated intro"), + Optional.of("Mentor - I am looking for guidance from someone with more experience"), + Optional.of("Technology"), + Optional.of("Software Engineer"), + Optional.of("AI,ML"), + Optional.of("Notes")); when(memberRepo.getMemberById(id)).thenReturn(Optional.empty()); @@ -136,8 +136,17 @@ void updateMember_throwsExceptionWhenMemberNotFound() { @Test void updateMember_successWithOnlyNameField() { final UUID id = UUID.randomUUID(); - final UpdateMemberRequest request = - new UpdateMemberRequest("UpdatedFirstName", null, null, null, null, null, null, null, null, null); + final UpdateMemberRequest request = new UpdateMemberRequest( + Optional.of("UpdatedFirstName"), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty()); final Member existingMember = Member.builder() .id(id) @@ -181,8 +190,17 @@ void updateMember_successWithOnlyNameField() { @Test void updateMember_successWithAllNullFields() { final UUID id = UUID.randomUUID(); - final UpdateMemberRequest request = - new UpdateMemberRequest(null, null, null, null, null, null, null, null, null, null); + final UpdateMemberRequest request = new UpdateMemberRequest( + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty()); final Member existingMember = Member.builder() .id(id) @@ -214,16 +232,16 @@ void updateMember_successWithAllNullFields() { void updateMember_successWithAllFields() { final UUID id = UUID.randomUUID(); final UpdateMemberRequest request = new UpdateMemberRequest( - "UpdatedFirstName", - "UpdatedLastName", - "updated@example.com", - "https://linkedin.com/in/updated", - "Updated intro", - "Mentor", - "Tech", - "Engineer", - "AI,ML", - "Notes"); + Optional.of("UpdatedFirstName"), + Optional.of("UpdatedLastName"), + Optional.of("updated@example.com"), + Optional.of("https://linkedin.com/in/updated"), + Optional.of("Updated intro"), + Optional.of("Mentor"), + Optional.of("Tech"), + Optional.of("Engineer"), + Optional.of("AI,ML"), + Optional.of("Notes")); final Member existingMember = Member.builder() .id(id) @@ -241,16 +259,16 @@ void updateMember_successWithAllFields() { final Member updatedMember = Member.builder() .id(id) - .firstName(request.firstName()) - .lastName(request.lastName()) - .email(request.email()) - .linkedInUrl(request.linkedInUrl()) - .introduction(request.introduction()) - .matchPref(request.matchPref()) - .industryPref(request.industryPref()) - .rolePref(request.rolePref()) - .topics(request.topics()) - .extraNotes(request.extraNotes()) + .firstName("UpdatedFirstName") + .lastName("UpdatedLastName") + .email("updated@example.com") + .linkedInUrl("https://linkedin.com/in/updated") + .introduction("Updated intro") + .matchPref("Mentor") + .industryPref("Tech") + .rolePref("Engineer") + .topics("AI,ML") + .extraNotes("Notes") .build(); when(memberRepo.getMemberById(id)).thenReturn(Optional.of(existingMember)); @@ -258,15 +276,86 @@ void updateMember_successWithAllFields() { final MemberDto response = memberService.updateMember(request, id); - assertEquals(request.firstName(), response.getFirstName()); - assertEquals(request.lastName(), response.getLastName()); - assertEquals(request.email(), response.getEmail()); - assertEquals(request.linkedInUrl(), response.getLinkedInUrl()); - assertEquals(request.introduction(), response.getIntroduction()); - assertEquals(request.matchPref(), response.getMatchPref()); - assertEquals(request.industryPref(), response.getIndustryPref()); - assertEquals(request.rolePref(), response.getRolePref()); - assertEquals(request.topics(), response.getTopics()); - assertEquals(request.extraNotes(), response.getExtraNotes()); + assertEquals("UpdatedFirstName", response.getFirstName()); + assertEquals("UpdatedLastName", response.getLastName()); + assertEquals("updated@example.com", response.getEmail()); + assertEquals("https://linkedin.com/in/updated", response.getLinkedInUrl()); + assertEquals("Updated intro", response.getIntroduction()); + assertEquals("Mentor", response.getMatchPref()); + assertEquals("Tech", response.getIndustryPref()); + assertEquals("Engineer", response.getRolePref()); + assertEquals("AI,ML", response.getTopics()); + assertEquals("Notes", response.getExtraNotes()); + } + + @Test + void updateMember_throwsExceptionWhenEmailIsDuplicate() { + final UUID id = UUID.randomUUID(); + final UUID otherMemberId = UUID.randomUUID(); + final UpdateMemberRequest request = new UpdateMemberRequest( + Optional.empty(), + Optional.empty(), + Optional.of("existing@example.com"), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty()); + + final Member existingMember = Member.builder() + .id(id) + .firstName("John") + .lastName("Doe") + .email("john@example.com") + .build(); + + final Member otherMember = + Member.builder().id(otherMemberId).email("existing@example.com").build(); + + when(memberRepo.getMemberById(id)).thenReturn(Optional.of(existingMember)); + when(memberRepo.getMemberByEmail("existing@example.com")).thenReturn(Optional.of(otherMember)); + + assertThrows(MemberDuplicateException.class, () -> memberService.updateMember(request, id)); + verify(memberRepo, never()).updateMember(any()); + } + + @Test + void updateMember_successWhenUpdatingWithSameEmail() { + final UUID id = UUID.randomUUID(); + final UpdateMemberRequest request = new UpdateMemberRequest( + Optional.empty(), + Optional.empty(), + Optional.of("john@example.com"), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.empty()); + + final Member existingMember = Member.builder() + .id(id) + .firstName("John") + .lastName("Doe") + .email("john@example.com") + .linkedInUrl("https://linkedin.com/in/john") + .introduction("intro") + .matchPref("Mentor") + .industryPref("Tech") + .rolePref("Engineer") + .topics("AI") + .extraNotes("notes") + .build(); + + when(memberRepo.getMemberById(id)).thenReturn(Optional.of(existingMember)); + when(memberRepo.updateMember(any())).thenReturn(Optional.of(existingMember)); + + final MemberDto response = memberService.updateMember(request, id); + + assertEquals("john@example.com", response.getEmail()); + verify(memberRepo, never()).getMemberByEmail(any()); } }