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..8a5fee9 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,10 @@ package org.patinanetwork.patchats.api.member; import java.util.List; +import java.util.Optional; import java.util.UUID; +import java.util.function.Consumer; +import java.util.stream.Stream; import lombok.RequiredArgsConstructor; import org.patinanetwork.patchats.api.member.db.models.Member; import org.patinanetwork.patchats.api.member.db.repos.MemberRepo; @@ -10,6 +13,8 @@ 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.dao.DuplicateKeyException; import org.springframework.stereotype.Service; @Service @@ -48,38 +53,51 @@ 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()); + // Return early if no fields are present in the request to update + boolean hasNoUpdates = Stream.of( + request.firstName(), + request.lastName(), + request.email(), + request.introduction(), + request.linkedInUrl(), + request.matchPref(), + request.industryPref(), + request.rolePref(), + request.topics(), + request.extraNotes()) + .noneMatch(Optional::isPresent); + if (hasNoUpdates) { + return MemberDto.from(member); } - 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()); + + validateAndUpdate(request.firstName(), member::setFirstName, "firstName", true); + validateAndUpdate(request.lastName(), member::setLastName, "lastName", true); + validateAndUpdate(request.email(), member::setEmail, "email", true); + validateAndUpdate(request.introduction(), member::setIntroduction, "introduction", true); + validateAndUpdate(request.linkedInUrl(), member::setLinkedInUrl, "linkedInUrl", false); + validateAndUpdate(request.matchPref(), member::setMatchPref, "matchPref", false); + validateAndUpdate(request.industryPref(), member::setIndustryPref, "industryPref", false); + validateAndUpdate(request.rolePref(), member::setRolePref, "rolePref", false); + validateAndUpdate(request.topics(), member::setTopics, "topics", false); + validateAndUpdate(request.extraNotes(), member::setExtraNotes, "extraNotes", false); + + try { + Member updatedMember = memberRepo.updateMember(member).orElseThrow(() -> new MemberNotFoundException(id)); + return MemberDto.from(updatedMember); + } catch (DuplicateKeyException e) { + throw new MemberDuplicateException(member.getEmail()); } - if (request.extraNotes() != null) { - member.setExtraNotes(request.extraNotes()); + } + + private void validateAndUpdate( + Optional field, Consumer setter, String fieldName, boolean required) { + if (field.isPresent()) { + String value = field.get(); + if (value.isBlank() && required) { + throw new ValidationException(fieldName + " cannot be empty"); + } + setter.accept(value); } - Member updatedMember = memberRepo.updateMember(member).orElseThrow(() -> new MemberNotFoundException(id)); - return MemberDto.from(updatedMember); } // TODO: Implement these methods after createMember and updateMember is fully functional and tested 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 412c1d2..0c75e40 100644 --- a/src/main/java/org/patinanetwork/patchats/common/web/ApiExceptionHandler.java +++ b/src/main/java/org/patinanetwork/patchats/common/web/ApiExceptionHandler.java @@ -7,6 +7,7 @@ import org.patinanetwork.patchats.common.web.exception.EmailTemplateNotFoundException; 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; @@ -52,6 +53,11 @@ public ResponseEntity> handleEmailNotResendable(final EmailNo 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..e463e11 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,20 +40,16 @@ 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 { - final CreateMemberRequest request = new CreateMemberRequest( - "John", - "Doe", - "john.doe@example.com", - "https://www.linkedin.com/in/johndoe", - "Hello, I'm John!", - "Friend", - "Mentor - I am looking for guidance from someone with more experience", - "Technology", - "Software Engineer", - "College, Career Development", - "I want to be meet someone in person in NYC"); + void createMember_returnsOkAndMemberDto() throws Exception { + final CreateMemberRequest request = MemberTestFixtures.CREATE_REQUEST_ALL_FIELDS; when(memberService.createMember(any())) .thenReturn(MemberDto.builder() @@ -106,4 +111,204 @@ 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 = MemberTestFixtures.UPDATE_REQUEST_ALL_FIELDS; + + 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..24317dd 100644 --- a/src/test/java/org/patinanetwork/patchats/api/member/MemberServiceTest.java +++ b/src/test/java/org/patinanetwork/patchats/api/member/MemberServiceTest.java @@ -13,6 +13,7 @@ import java.util.Optional; import java.util.UUID; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.patinanetwork.patchats.api.member.db.models.Member; import org.patinanetwork.patchats.api.member.db.repos.MemberRepo; import org.patinanetwork.patchats.api.member.dto.CreateMemberRequest; @@ -20,6 +21,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.springframework.dao.DuplicateKeyException; class MemberServiceTest { @@ -28,18 +30,7 @@ class MemberServiceTest { @Test void createMemberSuccessWithAllFieldsFilled() { - final CreateMemberRequest request = new CreateMemberRequest( - "John", - "Doe", - "john.doe@example.com", - "https://www.linkedin.com/in/johndoe", - "Hello, I'm John!", - "Friend", - "Mentor - I am looking for guidance from someone with more experience", - "Technology", - "Software Engineer", - "College, Career Development", - "I want to be meet someone in person in NYC"); + final CreateMemberRequest request = MemberTestFixtures.CREATE_REQUEST_ALL_FIELDS; when(memberRepo.getMemberByEmail(any())).thenReturn(Optional.empty()); when(memberRepo.createMember(any())) @@ -78,18 +69,7 @@ void createMemberSuccessWithAllFieldsFilled() { @Test void createMemberThrowsExceptionWhenEmailAlreadyExists() { - final CreateMemberRequest request = new CreateMemberRequest( - "John", - "Doe", - "john.doe@example.com", - "https://www.linkedin.com/in/johndoe", - "Hello, I'm John!", - "Friend", - "Mentor - I am looking for guidance from someone with more experience", - "Technology", - "Software Engineer", - "College, Career Development", - "I want to be meet someone in person in NYC"); + final CreateMemberRequest request = MemberTestFixtures.CREATE_REQUEST_ALL_FIELDS; when(memberRepo.getMemberByEmail(any())) .thenReturn(Optional.of(Member.builder() @@ -115,17 +95,7 @@ void createMemberThrowsExceptionWhenEmailAlreadyExists() { @Test 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"); + final UpdateMemberRequest request = MemberTestFixtures.UPDATE_REQUEST_ALL_FIELDS; when(memberRepo.getMemberById(id)).thenReturn(Optional.empty()); @@ -136,8 +106,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) @@ -153,36 +132,36 @@ void updateMember_successWithOnlyNameField() { .extraNotes("Old notes") .build(); - final Member updatedMember = Member.builder() - .id(id) - .firstName("UpdatedFirstName") - .lastName("OldLastName") - .email("old@example.com") - .linkedInUrl("https://linkedin.com/in/old") - .introduction("Old intro") - .matchPref("Mentor") - .industryPref("Finance") - .rolePref("Analyst") - .topics("Economics") - .extraNotes("Old notes") - .build(); + final ArgumentCaptor captor = ArgumentCaptor.forClass(Member.class); when(memberRepo.getMemberById(id)).thenReturn(Optional.of(existingMember)); - when(memberRepo.updateMember(any())).thenReturn(Optional.of(updatedMember)); + when(memberRepo.updateMember(any(Member.class))).thenReturn(Optional.of(existingMember)); - final MemberDto response = memberService.updateMember(request, id); + memberService.updateMember(request, id); - assertEquals("UpdatedFirstName", response.getFirstName()); - assertEquals("old@example.com", response.getEmail()); - assertEquals("https://linkedin.com/in/old", response.getLinkedInUrl()); - assertEquals("Old intro", response.getIntroduction()); + verify(memberRepo).updateMember(captor.capture()); + final Member captured = captor.getValue(); + + assertEquals("UpdatedFirstName", captured.getFirstName()); + assertEquals("old@example.com", captured.getEmail()); + assertEquals("https://linkedin.com/in/old", captured.getLinkedInUrl()); + assertEquals("Old intro", captured.getIntroduction()); } @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) @@ -199,31 +178,21 @@ void updateMember_successWithAllNullFields() { .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("OldFirstName", response.getFirstName()); assertEquals("OldLastName", response.getLastName()); assertEquals("email@example.com", response.getEmail()); assertEquals("https://linkedin.com/in/john", response.getLinkedInUrl()); assertEquals("intro", response.getIntroduction()); + + verify(memberRepo, never()).updateMember(any()); } @Test 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"); + final UpdateMemberRequest request = MemberTestFixtures.UPDATE_REQUEST_ALL_FIELDS; final Member existingMember = Member.builder() .id(id) @@ -239,34 +208,93 @@ void updateMember_successWithAllFields() { .extraNotes("Old notes") .build(); - final Member updatedMember = Member.builder() + final ArgumentCaptor captor = ArgumentCaptor.forClass(Member.class); + + when(memberRepo.getMemberById(id)).thenReturn(Optional.of(existingMember)); + when(memberRepo.updateMember(any(Member.class))).thenReturn(Optional.of(existingMember)); + + memberService.updateMember(request, id); + + verify(memberRepo).updateMember(captor.capture()); + final Member captured = captor.getValue(); + + assertEquals("UpdatedFirstName", captured.getFirstName()); + assertEquals("UpdatedLastName", captured.getLastName()); + assertEquals("updated@example.com", captured.getEmail()); + assertEquals("https://linkedin.com/in/updated", captured.getLinkedInUrl()); + assertEquals("Updated intro", captured.getIntroduction()); + assertEquals("Mentor - I am looking for guidance from someone with more experience", captured.getMatchPref()); + assertEquals("Technology", captured.getIndustryPref()); + assertEquals("Software Engineer", captured.getRolePref()); + assertEquals("AI,ML", captured.getTopics()); + assertEquals("Notes", captured.getExtraNotes()); + } + + @Test + void updateMember_throwsExceptionWhenEmailIsDuplicate() { + 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()); + + final Member existingMember = 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("John") + .lastName("Doe") + .email("john@example.com") .build(); + when(memberRepo.updateMember(any(Member.class))).thenThrow(new DuplicateKeyException("Email already exists")); when(memberRepo.getMemberById(id)).thenReturn(Optional.of(existingMember)); - when(memberRepo.updateMember(any())).thenReturn(Optional.of(updatedMember)); + assertThrows(MemberDuplicateException.class, () -> memberService.updateMember(request, id)); + } - final MemberDto response = memberService.updateMember(request, id); + @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()); - 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()); + 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 ArgumentCaptor captor = ArgumentCaptor.forClass(Member.class); + memberService.updateMember(request, id); + verify(memberRepo).updateMember(captor.capture()); + final Member captured = captor.getValue(); + + assertEquals("john@example.com", captured.getEmail()); + verify(memberRepo, never()).getMemberByEmail(any()); } } diff --git a/src/test/java/org/patinanetwork/patchats/api/member/MemberTestFixtures.java b/src/test/java/org/patinanetwork/patchats/api/member/MemberTestFixtures.java new file mode 100644 index 0000000..78d7910 --- /dev/null +++ b/src/test/java/org/patinanetwork/patchats/api/member/MemberTestFixtures.java @@ -0,0 +1,35 @@ +package org.patinanetwork.patchats.api.member; + +import java.util.Optional; +import org.patinanetwork.patchats.api.member.dto.CreateMemberRequest; +import org.patinanetwork.patchats.api.member.dto.UpdateMemberRequest; + +final class MemberTestFixtures { + + static final CreateMemberRequest CREATE_REQUEST_ALL_FIELDS = new CreateMemberRequest( + "John", + "Doe", + "john.doe@example.com", + "https://www.linkedin.com/in/johndoe", + "Hello, I'm John!", + "Friend", + "Mentor - I am looking for guidance from someone with more experience", + "Technology", + "Software Engineer", + "College, Career Development", + "I want to be meet someone in person in NYC"); + + static final UpdateMemberRequest UPDATE_REQUEST_ALL_FIELDS = 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 - I am looking for guidance from someone with more experience"), + Optional.of("Technology"), + Optional.of("Software Engineer"), + Optional.of("AI,ML"), + Optional.of("Notes")); + + private MemberTestFixtures() {} +}