diff --git a/admin-wcc-app/__tests__/pages/admin/members/create.test.tsx b/admin-wcc-app/__tests__/pages/admin/members/create.test.tsx new file mode 100644 index 00000000..305319e1 --- /dev/null +++ b/admin-wcc-app/__tests__/pages/admin/members/create.test.tsx @@ -0,0 +1,97 @@ +import '@testing-library/jest-dom'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import CreateMemberPage from '@/pages/admin/members/create'; +import * as api from '@/lib/api'; +import * as AuthProvider from '@/components/AuthProvider'; +import mockRouter from 'next-router-mock'; + +jest.mock('next/router', () => jest.requireActual('next-router-mock')); +jest.mock('@/lib/api'); +jest.mock('@/components/AuthProvider'); +jest.mock('@/lib/auth', () => ({ + getStoredToken: jest.fn(() => 'mock-token'), +})); + +global.scrollTo = jest.fn(); + +const mockApiFetch = api.apiFetch as jest.MockedFunction; +const mockUseAuth = AuthProvider.useAuth as jest.Mock; + +describe('CreateMemberPage', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockRouter.setCurrentUrl('/admin/members/create'); + + mockUseAuth.mockReturnValue({ + token: 'mock-token', + roles: ['ADMIN'], + member: null, + logout: jest.fn(), + }); + }); + + const fillRequiredFields = async (user: ReturnType) => { + await user.type(screen.getByLabelText(/full name/i), ' Jane Doe '); + await user.type(screen.getByLabelText(/email/i), 'jane@example.com'); + await user.type(screen.getByLabelText(/position/i), 'Developer'); + await user.type(screen.getByLabelText(/slack display name/i), ' jane doe '); + + const countryInput = screen.getByLabelText(/country/i); + await user.click(countryInput); + await user.type(countryInput, 'United'); + + const countryOption = await screen.findByRole('option', { + name: /united states \(us\)/i, + }); + await user.click(countryOption); + + const memberTypesInput = screen.getByLabelText(/member types/i); + await user.click(memberTypesInput); + + const memberOption = await screen.findByRole('option', { + name: /^member$/i, + }); + await user.click(memberOption); + }; + + it('trims full name and Slack display name before submitting member data', async () => { + mockApiFetch.mockResolvedValueOnce({}); + + const user = userEvent.setup(); + + render(); + await fillRequiredFields(user); + + await user.click(screen.getByRole('button', { name: /create member/i })); + + await waitFor(() => { + expect(mockApiFetch).toHaveBeenCalled(); + }); + + const payload = mockApiFetch.mock.calls[0][1]?.body; + + expect(payload).toMatchObject({ + fullName: 'Jane Doe', + slackDisplayName: 'jane doe', + }); + }); + + it.each([ + ['full name', /full name/i, 'Full name is required'], + ['Slack display name', /slack display name/i, 'Slack display name is required'], + ])('rejects a whitespace-only %s', async (_field, label, message) => { + const user = userEvent.setup(); + + render(); + + await fillRequiredFields(user); + const input = screen.getByLabelText(label); + await user.clear(input); + await user.type(input, ' '); + await user.click(screen.getByRole('button', { name: /create member/i })); + + expect(await screen.findByText(message)).toBeInTheDocument(); + expect(mockApiFetch).not.toHaveBeenCalled(); + }); +}); diff --git a/admin-wcc-app/pages/admin/members/create.tsx b/admin-wcc-app/pages/admin/members/create.tsx index 20ec792a..d1ca2d76 100644 --- a/admin-wcc-app/pages/admin/members/create.tsx +++ b/admin-wcc-app/pages/admin/members/create.tsx @@ -127,11 +127,15 @@ export default function CreateMemberPage() { const validateForm = () => { const errors: Record = {}; - if (!formData.fullName) errors.fullName = 'Full name is required'; + if (!formData.fullName.trim()) { + errors.fullName = 'Full name is required'; + } if (!formData.position) errors.position = 'Position is required'; if (!formData.email) errors.email = 'Email is required'; else if (!validateEmail(formData.email)) errors.email = 'Invalid email format'; - if (!formData.slackDisplayName) errors.slackDisplayName = 'Slack display name is required'; + if (!formData.slackDisplayName.trim()) { + errors.slackDisplayName = 'Slack display name is required'; + } if (!formData.country?.countryCode) errors.country = 'Country is required'; if (!formData.memberTypes || formData.memberTypes.length === 0) { errors.memberTypes = 'At least one member type is required'; @@ -166,6 +170,8 @@ export default function CreateMemberPage() { const submitData = { ...restFormData, + fullName: restFormData.fullName.trim(), + slackDisplayName: restFormData.slackDisplayName.trim(), country: { countryCode: country?.countryCode, countryName: country?.countryName, diff --git a/src/main/java/com/wcc/platform/repository/postgres/component/MemberMapper.java b/src/main/java/com/wcc/platform/repository/postgres/component/MemberMapper.java index 5c376b77..c0b2116c 100644 --- a/src/main/java/com/wcc/platform/repository/postgres/component/MemberMapper.java +++ b/src/main/java/com/wcc/platform/repository/postgres/component/MemberMapper.java @@ -103,7 +103,7 @@ public Long addMember(final Member member) { final int defaultStatusPending = 1; jdbc.update( INSERT, - member.getFullName(), + StringUtils.trim(member.getFullName()), member.getSlackDisplayName(), member.getPosition(), member.getCompanyName(), @@ -153,7 +153,7 @@ private Long findMemberIdByEmail(final String email) { public void updateMember(final Member member, final Long memberId) { jdbc.update( UPDATE_SQL, - member.getFullName(), + StringUtils.trim(member.getFullName()), member.getSlackDisplayName(), member.getPosition(), member.getCompanyName(), diff --git a/src/test/java/com/wcc/platform/repository/postgres/component/MemberMapperTest.java b/src/test/java/com/wcc/platform/repository/postgres/component/MemberMapperTest.java index 3e07850e..38f154d0 100644 --- a/src/test/java/com/wcc/platform/repository/postgres/component/MemberMapperTest.java +++ b/src/test/java/com/wcc/platform/repository/postgres/component/MemberMapperTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; @@ -87,7 +88,7 @@ void testMapRowToMember() throws SQLException { @Test void testAddMember() { Member member = mock(Member.class); - when(member.getFullName()).thenReturn("Jane Doe"); + when(member.getFullName()).thenReturn(" Jane Doe "); when(member.getSlackDisplayName()).thenReturn("jane"); when(member.getPosition()).thenReturn("Manager"); when(member.getCompanyName()).thenReturn("WCC"); @@ -111,6 +112,22 @@ void testAddMember() { Long memberId = memberMapper.addMember(member); assertEquals(10L, memberId); + + verify(jdbc) + .update( + anyString(), + eq("Jane Doe"), + any(), + any(), + any(), + any(), + any(), + any(), + any(), + any(), + any(), + any()); + verify(memberTypeRepo, never()).addMemberType(anyLong(), anyInt()); verify(socialNetworkRepo, never()).addSocialNetwork(anyLong(), any()); } @@ -118,7 +135,7 @@ void testAddMember() { @Test void testUpdateMember() { Member member = mock(Member.class); - when(member.getFullName()).thenReturn("Jane Doe"); + when(member.getFullName()).thenReturn(" Jane Doe "); when(member.getSlackDisplayName()).thenReturn("jane"); when(member.getPosition()).thenReturn("Manager"); when(member.getCompanyName()).thenReturn("WCC"); @@ -139,7 +156,7 @@ void testUpdateMember() { verify(jdbc) .update( anyString(), - any(), + eq("Jane Doe"), any(), any(), any(),