From 4ec82f8301cb7f6ac9a7ad437e0a3cb70cbccbd4 Mon Sep 17 00:00:00 2001 From: Allison Lee Date: Fri, 17 Jul 2026 15:30:42 -0400 Subject: [PATCH] Instant to OffsetDateTime --- js/src/features/member-profile/api/schemas.ts | 2 +- js/src/features/member-profile/types.ts | 2 +- .../sign-up/components/SignUpForm.tsx | 36 +++-- .../patchats/api/member/MemberService.java | 10 +- .../patchats/api/member/db/models/Member.java | 7 +- .../api/member/db/repos/MemberSqlRepo.java | 5 +- .../patchats/api/member/dto/MemberDto.java | 6 +- .../api/member/MemberControllerTest.java | 106 +++++++++++++++ .../api/member/MemberServiceTest.java | 124 ++++++++++++++++++ 9 files changed, 269 insertions(+), 29 deletions(-) create mode 100644 src/test/java/org/patinanetwork/patchats/api/member/MemberControllerTest.java create mode 100644 src/test/java/org/patinanetwork/patchats/api/member/MemberServiceTest.java diff --git a/js/src/features/member-profile/api/schemas.ts b/js/src/features/member-profile/api/schemas.ts index dcc308b..4cb0e21 100644 --- a/js/src/features/member-profile/api/schemas.ts +++ b/js/src/features/member-profile/api/schemas.ts @@ -25,7 +25,7 @@ export const memberProfileSchema = z.object({ referralSource: z .string() .max(200, "Referral source must be 200 characters or fewer."), - matchingPref: z.string(), + matchPref: z.string(), industryPref: z.string(), rolePref: z .string() diff --git a/js/src/features/member-profile/types.ts b/js/src/features/member-profile/types.ts index 06b19d7..1d15cb2 100644 --- a/js/src/features/member-profile/types.ts +++ b/js/src/features/member-profile/types.ts @@ -4,7 +4,7 @@ export interface MemberProfileValues { linkedInUrl: string; introduction: string; referralSource: string; - matchingPref: string; + matchPref: string; industryPref: string; rolePref: string; topics: string; diff --git a/js/src/features/sign-up/components/SignUpForm.tsx b/js/src/features/sign-up/components/SignUpForm.tsx index d89bdec..4aeb074 100644 --- a/js/src/features/sign-up/components/SignUpForm.tsx +++ b/js/src/features/sign-up/components/SignUpForm.tsx @@ -20,7 +20,7 @@ import { import { useState } from "react"; // Options for select fields. Defined in SignUpFormConfig -const matchingPrefOptions = MATCHING_PREFERENCES.map((v) => ({ +const matchingPreferenceOptions = MATCHING_PREFERENCES.map((v) => ({ value: v, label: v, })); @@ -33,7 +33,7 @@ const initialFormValues: MemberProfileValues = { linkedInUrl: "", introduction: "", referralSource: "", - matchingPref: "", + matchPref: "", industryPref: "", rolePref: "", topics: "", @@ -122,12 +122,24 @@ export function SignUpForm() { setIsSubmitting(true); try { - // TODO: replace with API call - await Promise.resolve(); - setSuccessMessage("Your form was submitted successfully."); + const response = await fetch("/api/members", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(values), + }); + if (response.ok) { + setSuccessMessage("Your form was submitted successfully."); + setValues({ ...initialFormValues }); + } else { + const data = await response.json(); + setSubmitError( + data.message || + "An error occurred while submitting the form. Please try again later or contact an administrator.", + ); + } } catch (_error) { setSubmitError( - "There was a problem submitting the form. Please try again.", + "Network error - please check your connection and try again", ); } finally { setIsSubmitting(false); @@ -206,16 +218,16 @@ export function SignUpForm() { new ResponseStatusException(HttpStatus.NOT_FOUND, "Member not found")); + Member updatedMember = memberRepo.updateMember(member).orElseThrow(() -> new MemberNotFoundException(id)); return MemberDto.from(updatedMember); } @@ -72,9 +68,9 @@ public MemberDto updateMember(UpdateMemberRequest request, UUID id) { // .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Member not found")); // } - // public MemberDto deactivateMemberById(UUID id) { + // public MemberDto deactivateMember(UUID id) { // return memberRepo - // .deactivateMemberById(id) + // .deactivateMember(id) // .map(MemberDto::from) // .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Member not found")); // } diff --git a/src/main/java/org/patinanetwork/patchats/api/member/db/models/Member.java b/src/main/java/org/patinanetwork/patchats/api/member/db/models/Member.java index 9ecbbd6..56ad188 100644 --- a/src/main/java/org/patinanetwork/patchats/api/member/db/models/Member.java +++ b/src/main/java/org/patinanetwork/patchats/api/member/db/models/Member.java @@ -1,6 +1,6 @@ package org.patinanetwork.patchats.api.member.db.models; -import java.time.Instant; +import java.time.OffsetDateTime; import java.util.UUID; import lombok.Builder; import lombok.EqualsAndHashCode; @@ -10,6 +10,7 @@ @Getter @Builder +@Setter @ToString @EqualsAndHashCode public class Member { @@ -49,7 +50,7 @@ public class Member { @Setter private String extraNotes; - private Instant createdAt; + private OffsetDateTime createdAt; - private Instant updatedAt; + private OffsetDateTime updatedAt; } 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 f5a649c..a2dddd8 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 @@ -2,6 +2,7 @@ import java.sql.ResultSet; import java.sql.SQLException; +import java.time.OffsetDateTime; import java.util.Optional; import java.util.UUID; import lombok.RequiredArgsConstructor; @@ -29,8 +30,8 @@ private Member parseResultSetToMember(final ResultSet rs) throws SQLException { .rolePref(rs.getString("role_pref")) .topics(rs.getString("topics")) .extraNotes(rs.getString("extra_notes")) - .createdAt(rs.getTimestamp("created_at").toInstant()) - .updatedAt(rs.getTimestamp("updated_at").toInstant()) + .createdAt(rs.getObject("created_at", OffsetDateTime.class)) + .updatedAt(rs.getObject("updated_at", OffsetDateTime.class)) .build(); } diff --git a/src/main/java/org/patinanetwork/patchats/api/member/dto/MemberDto.java b/src/main/java/org/patinanetwork/patchats/api/member/dto/MemberDto.java index 503f4c1..96920ca 100644 --- a/src/main/java/org/patinanetwork/patchats/api/member/dto/MemberDto.java +++ b/src/main/java/org/patinanetwork/patchats/api/member/dto/MemberDto.java @@ -1,7 +1,7 @@ package org.patinanetwork.patchats.api.member.dto; import io.swagger.v3.oas.annotations.media.Schema; -import java.time.Instant; +import java.time.OffsetDateTime; import java.util.UUID; import lombok.Builder; import lombok.EqualsAndHashCode; @@ -53,10 +53,10 @@ public class MemberDto { private String extraNotes; @Schema(requiredMode = Schema.RequiredMode.REQUIRED) - private Instant createdAt; + private OffsetDateTime createdAt; @Schema(requiredMode = Schema.RequiredMode.REQUIRED) - private Instant updatedAt; + private OffsetDateTime updatedAt; public static MemberDto from(final Member member) { return MemberDto.builder() diff --git a/src/test/java/org/patinanetwork/patchats/api/member/MemberControllerTest.java b/src/test/java/org/patinanetwork/patchats/api/member/MemberControllerTest.java new file mode 100644 index 0000000..3a23b44 --- /dev/null +++ b/src/test/java/org/patinanetwork/patchats/api/member/MemberControllerTest.java @@ -0,0 +1,106 @@ +package org.patinanetwork.patchats.api.member; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +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 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.common.web.ApiExceptionHandler; +import org.patinanetwork.patchats.common.web.exception.MemberDuplicateException; +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; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(MemberController.class) +@AutoConfigureMockMvc(addFilters = false) +@Import(ApiExceptionHandler.class) +class MemberControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private MemberService memberService; + + @Test + void createMember_ReturnsOkAndMemberDto() 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"); + + when(memberService.createMember(any())) + .thenReturn(MemberDto.builder() + .id(UUID.randomUUID()) + .fullName(request.fullName()) + .email(request.email()) + .linkedInUrl(request.linkedInUrl()) + .introduction(request.introduction()) + .referralSource(request.referralSource()) + .active(true) + .matchPref(request.matchPref()) + .industryPref(request.industryPref()) + .rolePref(request.rolePref()) + .topics(request.topics()) + .extraNotes(request.extraNotes()) + .build()); + mockMvc.perform( + post("/api/members") + .contentType(MediaType.APPLICATION_JSON) + .content( + "{\"fullName\":\"John Doe\",\"email\":\"john.doe@example.com\",\"linkedInUrl\":\"https://www.linkedin.com/in/johndoe\",\"introduction\":\"Hello, I'm John!\",\"referralSource\":\"Friend\",\"matchPref\":\"Mentor - I am looking for guidance from someone with more experience\",\"industryPref\":\"Technology\",\"rolePref\":\"Software Engineer\",\"topics\":\"College, Career Development\",\"extraNotes\":\"I want to be meet someone in person in NYC\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.payload.fullName").value(request.fullName())) + .andExpect(jsonPath("$.payload.email").value(request.email())) + .andExpect(jsonPath("$.payload.linkedInUrl").value(request.linkedInUrl())) + .andExpect(jsonPath("$.payload.introduction").value(request.introduction())) + .andExpect(jsonPath("$.payload.referralSource").value(request.referralSource())) + .andExpect(jsonPath("$.payload.matchPref").value(request.matchPref())) + .andExpect(jsonPath("$.payload.industryPref").value(request.industryPref())) + .andExpect(jsonPath("$.payload.rolePref").value(request.rolePref())) + .andExpect(jsonPath("$.payload.topics").value(request.topics())) + .andExpect(jsonPath("$.payload.extraNotes").value(request.extraNotes())) + .andExpect(jsonPath("$.payload.id").isNotEmpty()) + .andExpect(jsonPath("$.payload.active").value(true)); + } + + @Test + void createMember_returnsBadRequestWhenFullNameIsBlank() throws Exception { + mockMvc.perform( + post("/api/members") + .contentType(MediaType.APPLICATION_JSON) + .content( + "{\"fullName\":\"\",\"email\":\"john.doe@example.com\",\"linkedInUrl\":\"https://www.linkedin.com/in/johndoe\",\"introduction\":\"Hello, I'm John!\",\"referralSource\":\"Friend\",\"matchPref\":\"Mentor - I am looking for guidance from someone with more experience\",\"industryPref\":\"Technology\",\"rolePref\":\"Software Engineer\",\"topics\":\"College, Career Development\",\"extraNotes\":\"I want to be meet someone in person in NYC\"}")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)); + } + + @Test + void createMember_returnsConflictOnDuplicateEmail() throws Exception { + when(memberService.createMember(any())).thenThrow(new MemberDuplicateException("john.doe@example.com")); + mockMvc.perform( + post("/api/members") + .contentType(MediaType.APPLICATION_JSON) + .content( + "{\"fullName\":\"John Doe\",\"email\":\"john.doe@example.com\",\"linkedInUrl\":\"https://www.linkedin.com/in/johndoe\",\"introduction\":\"Hello, I'm John!\",\"referralSource\":\"Friend\",\"matchPref\":\"Mentor - I am looking for guidance from someone with more experience\",\"industryPref\":\"Technology\",\"rolePref\":\"Software Engineer\",\"topics\":\"College, Career Development\",\"extraNotes\":\"I want to be meet someone in person in NYC\"}")) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.success").value(false)); + } +} diff --git a/src/test/java/org/patinanetwork/patchats/api/member/MemberServiceTest.java b/src/test/java/org/patinanetwork/patchats/api/member/MemberServiceTest.java new file mode 100644 index 0000000..c9e83d0 --- /dev/null +++ b/src/test/java/org/patinanetwork/patchats/api/member/MemberServiceTest.java @@ -0,0 +1,124 @@ +package org.patinanetwork.patchats.api.member; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import java.util.UUID; +import org.junit.jupiter.api.Test; +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; +import org.patinanetwork.patchats.api.member.dto.MemberDto; +import org.patinanetwork.patchats.common.web.exception.MemberDuplicateException; + +class MemberServiceTest { + + private final MemberRepo memberRepo = mock(MemberRepo.class); + private final MemberService memberService = new MemberService(memberRepo); + + @Test + void createMember_successWithAllFieldsFilled() { + 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"); + + when(memberRepo.getMemberByEmail(any())).thenReturn(Optional.empty()); + when(memberRepo.createMember(any())) + .thenReturn(Member.builder() + .id(UUID.randomUUID()) + .fullName(request.fullName()) + .email(request.email()) + .linkedInUrl(request.linkedInUrl()) + .introduction(request.introduction()) + .referralSource(request.referralSource()) + .active(true) + .matchPref(request.matchPref()) + .industryPref(request.industryPref()) + .rolePref(request.rolePref()) + .topics(request.topics()) + .extraNotes(request.extraNotes()) + .build()); + final MemberDto response = memberService.createMember(request); + + assertEquals(request.fullName(), response.getFullName()); + assertEquals(request.email(), response.getEmail()); + assertEquals(request.linkedInUrl(), response.getLinkedInUrl()); + assertEquals(request.introduction(), response.getIntroduction()); + assertEquals(request.referralSource(), response.getReferralSource()); + 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()); + + assertNotNull(response.getId()); + assertTrue(response.getActive()); + } + + @Test + void createMember_throwsExceptionWhenEmailAlreadyExists() { + 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"); + + when(memberRepo.getMemberByEmail(any())) + .thenReturn(Optional.of(Member.builder() + .id(UUID.randomUUID()) + .fullName(request.fullName()) + .email(request.email()) + .linkedInUrl(request.linkedInUrl()) + .introduction(request.introduction()) + .referralSource(request.referralSource()) + .active(true) + .matchPref(request.matchPref()) + .industryPref(request.industryPref()) + .rolePref(request.rolePref()) + .topics(request.topics()) + .extraNotes(request.extraNotes()) + .build())); + + assertThrows(MemberDuplicateException.class, () -> memberService.createMember(request)); + verify(memberRepo, never()).createMember(any()); + } + + // TODO: Implement test cases for MemberService methods after createMember + + // @Test + // void testUpdateMember() { + // // Implement test logic for updateMember method + // } + + // @Test + // void testGetMemberById() { + // // Implement test logic for getMemberById method + // } + + // @Test + // void testDeactivateMember() { + // // Implement test logic for deactivateMember method + // } +}