From 73a60329043e4085af558005a5fba0c41f017c30 Mon Sep 17 00:00:00 2001 From: Allison Lee Date: Fri, 7 Aug 2026 14:59:47 -0400 Subject: [PATCH 1/5] MemberSqlRepo updateMember and getMemberById --- .../patchats/api/member/MemberController.java | 17 +- .../patchats/api/member/MemberService.java | 69 ++++-- .../api/member/db/repos/MemberSqlRepo.java | 60 +++-- .../api/member/dto/UpdateMemberRequest.java | 21 +- .../common/web/ApiExceptionHandler.java | 6 + .../web/exception/ValidationException.java | 11 + .../api/member/MemberControllerTest.java | 222 +++++++++++++++++- .../api/member/MemberServiceTest.java | 177 ++++++++++---- 8 files changed, 485 insertions(+), 98 deletions(-) create mode 100644 src/main/java/org/patinanetwork/patchats/common/web/exception/ValidationException.java 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..d48ee60 100644 --- a/src/main/java/org/patinanetwork/patchats/api/member/MemberService.java +++ b/src/main/java/org/patinanetwork/patchats/api/member/MemberService.java @@ -10,6 +10,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,36 +49,64 @@ 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()); + // Validate that required fields are not empty + if (request.firstName().isPresent()) { + String firstName = request.firstName().get(); + if (firstName.isBlank()) { + throw new ValidationException("firstName cannot be empty"); + } + member.setFirstName(firstName); } - if (request.lastName() != null) { - member.setLastName(request.lastName()); + + if (request.lastName().isPresent()) { + String lastName = request.lastName().get(); + if (lastName.isBlank()) { + throw new ValidationException("lastName cannot be empty"); + } + member.setLastName(lastName); } - if (request.email() != null) { - member.setEmail(request.email()); + + if (request.email().isPresent()) { + String email = request.email().get(); + if (email.isBlank()) { + throw new ValidationException("email cannot be empty"); + } + if (!email.equals(member.getEmail())) { + if (memberRepo.getMemberByEmail(email).isPresent()) { + throw new MemberDuplicateException(email); + } + } + member.setEmail(email); } - if (request.linkedInUrl() != null) { - member.setLinkedInUrl(request.linkedInUrl()); + + if (request.introduction().isPresent()) { + String introduction = request.introduction().get(); + if (introduction.isBlank()) { + throw new ValidationException("introduction cannot be empty"); + } + member.setIntroduction(introduction); } - if (request.introduction() != null) { - member.setIntroduction(request.introduction()); + + // Update only the fields that are provided in the request + if (request.linkedInUrl().isPresent()) { + member.setLinkedInUrl(request.linkedInUrl().get()); } - if (request.matchPref() != null) { - member.setMatchPref(request.matchPref()); + if (request.matchPref().isPresent()) { + member.setMatchPref(request.matchPref().get()); } - if (request.industryPref() != null) { - member.setIndustryPref(request.industryPref()); + if (request.industryPref().isPresent()) { + member.setIndustryPref(request.industryPref().get()); } - if (request.rolePref() != null) { - member.setRolePref(request.rolePref()); + if (request.rolePref().isPresent()) { + member.setRolePref(request.rolePref().get()); } - if (request.topics() != null) { - member.setTopics(request.topics()); + if (request.topics().isPresent()) { + member.setTopics(request.topics().get()); } - if (request.extraNotes() != null) { - member.setExtraNotes(request.extraNotes()); + if (request.extraNotes().isPresent()) { + member.setExtraNotes(request.extraNotes().get()); } + Member updatedMember = memberRepo.updateMember(member).orElseThrow(() -> new MemberNotFoundException(id)); return MemberDto.from(updatedMember); } 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..04eaeb2 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,23 @@ 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 +90,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 +103,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..e4d7090 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,17 @@ package org.patinanetwork.patchats.api.member.dto; import jakarta.validation.constraints.Email; +import java.util.Optional; public record UpdateMemberRequest( - String firstName, - String lastName, + Optional firstName, + Optional 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<@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..0154fd7 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,25 @@ 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.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 +39,10 @@ class MemberControllerTest { @MockitoBean private MemberService memberService; + private final ObjectMapper objectMapper = new ObjectMapper(); + @Test - void createMemberReturnsOkAndMemberDto() throws Exception { + void createMember_returnsOkAndMemberDto() throws Exception { final CreateMemberRequest request = new CreateMemberRequest( "John", "Doe", @@ -106,4 +116,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("/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("/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 String invalidRequest = """ + { + "firstName": "", + "lastName": "UpdatedLastName", + "email": "updated@example.com", + "linkedInUrl": null, + "introduction": null, + "matchPref": null, + "industryPref": null, + "rolePref": null, + "topics": null, + "extraNotes": null + } + """; + + mockMvc.perform(patch("/members/{id}", id) + .contentType(MediaType.APPLICATION_JSON) + .content(invalidRequest)) + .andExpect(status().isBadRequest()); + } + + @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("/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("/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("/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("/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()); } } From 64d55658aaf189b4e70e1d6f8251c5ef713309b1 Mon Sep 17 00:00:00 2001 From: Allison Lee Date: Mon, 10 Aug 2026 16:58:40 -0400 Subject: [PATCH 2/5] Ran mvn spotless:apply --- .../patchats/api/member/db/repos/MemberSqlRepo.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 04eaeb2..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 @@ -38,8 +38,7 @@ private Member parseResultSetToMember(final ResultSet rs) throws SQLException { } private JdbcClient.StatementSpec bindMemberParams(JdbcClient.StatementSpec spec, Member member) { - return spec - .param("id", member.getId()) + return spec.param("id", member.getId()) .param("first_name", member.getFirstName()) .param("last_name", member.getLastName()) .param("email", member.getEmail()) From c7fd0e23f2817bfa3d07f835afb6c845affd3347 Mon Sep 17 00:00:00 2001 From: Allison Lee Date: Mon, 10 Aug 2026 17:39:46 -0400 Subject: [PATCH 3/5] Fixed test errors and added dependency to handle optional --- pom.xml | 4 ++ .../api/member/MemberControllerTest.java | 54 ++++++++++--------- 2 files changed, 34 insertions(+), 24 deletions(-) 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/test/java/org/patinanetwork/patchats/api/member/MemberControllerTest.java b/src/test/java/org/patinanetwork/patchats/api/member/MemberControllerTest.java index 0154fd7..05d20a3 100644 --- a/src/test/java/org/patinanetwork/patchats/api/member/MemberControllerTest.java +++ b/src/test/java/org/patinanetwork/patchats/api/member/MemberControllerTest.java @@ -12,6 +12,7 @@ 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; @@ -41,6 +42,11 @@ class MemberControllerTest { private final ObjectMapper objectMapper = new ObjectMapper(); + @BeforeEach + void setUp() { + objectMapper.registerModule(new com.fasterxml.jackson.datatype.jdk8.Jdk8Module()); + } + @Test void createMember_returnsOkAndMemberDto() throws Exception { final CreateMemberRequest request = new CreateMemberRequest( @@ -152,7 +158,7 @@ void updateMember_successWithValidRequest() throws Exception { when(memberService.updateMember(any(UpdateMemberRequest.class), eq(id))).thenReturn(response); - mockMvc.perform(patch("/members/{id}", id) + mockMvc.perform(patch("/api/members/{id}", id) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isOk()) @@ -199,7 +205,7 @@ void updateMember_successWithPartialUpdate() throws Exception { when(memberService.updateMember(any(UpdateMemberRequest.class), eq(id))).thenReturn(response); - mockMvc.perform(patch("/members/{id}", id) + mockMvc.perform(patch("/api/members/{id}", id) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isOk()) @@ -210,25 +216,25 @@ void updateMember_successWithPartialUpdate() throws Exception { @Test void updateMember_badRequestWhenBlankRequiredField() throws Exception { final UUID id = UUID.randomUUID(); - final String invalidRequest = """ - { - "firstName": "", - "lastName": "UpdatedLastName", - "email": "updated@example.com", - "linkedInUrl": null, - "introduction": null, - "matchPref": null, - "industryPref": null, - "rolePref": null, - "topics": null, - "extraNotes": null - } - """; - - mockMvc.perform(patch("/members/{id}", id) + 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(invalidRequest)) - .andExpect(status().isBadRequest()); + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("firstName cannot be empty")); } @Test @@ -249,7 +255,7 @@ void updateMember_notFoundWhenMemberDoesNotExist() throws Exception { when(memberService.updateMember(any(UpdateMemberRequest.class), eq(id))) .thenThrow(new MemberNotFoundException(id)); - mockMvc.perform(patch("/members/{id}", id) + mockMvc.perform(patch("/api/members/{id}", id) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isNotFound()) @@ -274,7 +280,7 @@ void updateMember_conflictWhenEmailIsDuplicate() throws Exception { when(memberService.updateMember(any(UpdateMemberRequest.class), eq(id))) .thenThrow(new MemberDuplicateException("existing@example.com")); - mockMvc.perform(patch("/members/{id}", id) + mockMvc.perform(patch("/api/members/{id}", id) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isConflict()) @@ -299,7 +305,7 @@ void updateMember_badRequestWhenValidationFails() throws Exception { when(memberService.updateMember(any(UpdateMemberRequest.class), eq(id))) .thenThrow(new ValidationException("firstName cannot be empty")); - mockMvc.perform(patch("/members/{id}", id) + mockMvc.perform(patch("/api/members/{id}", id) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isBadRequest()) @@ -321,7 +327,7 @@ void updateMember_badRequestWhenInvalidUuid() throws Exception { Optional.empty(), Optional.empty()); - mockMvc.perform(patch("/members/{id}", "invalid-uuid") + mockMvc.perform(patch("/api/members/{id}", "invalid-uuid") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isBadRequest()); From b8a550da9b4f61deeeffbae2ab1796e392102955 Mon Sep 17 00:00:00 2001 From: Allison Lee Date: Mon, 10 Aug 2026 18:06:44 -0400 Subject: [PATCH 4/5] Addressed auto code analysis - refactored MemberService --- .../patchats/api/member/MemberService.java | 81 ++++++++----------- 1 file changed, 34 insertions(+), 47 deletions(-) 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 d48ee60..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; @@ -49,66 +51,51 @@ public List getMembers() { public MemberDto updateMember(UpdateMemberRequest request, UUID id) { Member member = memberRepo.getMemberById(id).orElseThrow(() -> new MemberNotFoundException(id)); - // Validate that required fields are not empty - if (request.firstName().isPresent()) { - String firstName = request.firstName().get(); - if (firstName.isBlank()) { - throw new ValidationException("firstName cannot be empty"); - } - member.setFirstName(firstName); - } + validateAndUpdateRequired(member, request); + updateOptional(member, request); + + Member updatedMember = memberRepo.updateMember(member).orElseThrow(() -> new MemberNotFoundException(id)); + return MemberDto.from(updatedMember); + } - if (request.lastName().isPresent()) { - String lastName = request.lastName().get(); - if (lastName.isBlank()) { - throw new ValidationException("lastName cannot be empty"); + 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"); } - member.setLastName(lastName); + 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())) { - if (memberRepo.getMemberByEmail(email).isPresent()) { - throw new MemberDuplicateException(email); - } + if (!email.equals(member.getEmail()) + && memberRepo.getMemberByEmail(email).isPresent()) { + throw new MemberDuplicateException(email); } member.setEmail(email); } + } - if (request.introduction().isPresent()) { - String introduction = request.introduction().get(); - if (introduction.isBlank()) { - throw new ValidationException("introduction cannot be empty"); - } - member.setIntroduction(introduction); - } - - // Update only the fields that are provided in the request - if (request.linkedInUrl().isPresent()) { - member.setLinkedInUrl(request.linkedInUrl().get()); - } - if (request.matchPref().isPresent()) { - member.setMatchPref(request.matchPref().get()); - } - if (request.industryPref().isPresent()) { - member.setIndustryPref(request.industryPref().get()); - } - if (request.rolePref().isPresent()) { - member.setRolePref(request.rolePref().get()); - } - if (request.topics().isPresent()) { - member.setTopics(request.topics().get()); - } - if (request.extraNotes().isPresent()) { - member.setExtraNotes(request.extraNotes().get()); - } - - Member updatedMember = memberRepo.updateMember(member).orElseThrow(() -> new MemberNotFoundException(id)); - return MemberDto.from(updatedMember); + 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 From e2829d8abf4c9cf43bfd98fca48bccb908da35d1 Mon Sep 17 00:00:00 2001 From: Allison Lee Date: Mon, 10 Aug 2026 18:15:05 -0400 Subject: [PATCH 5/5] Removed comment in UpdateMemberRequest --- .../patchats/api/member/dto/UpdateMemberRequest.java | 1 - 1 file changed, 1 deletion(-) 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 e4d7090..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 @@ -6,7 +6,6 @@ public record UpdateMemberRequest( Optional firstName, Optional lastName, - // TODO: Changes to email require verification after authentication is implemented Optional<@Email String> email, Optional linkedInUrl, Optional introduction,