From 39e9d8c5f046f9160fdf9769798036b01e287760 Mon Sep 17 00:00:00 2001 From: Iryna Lypnyk Date: Mon, 28 Sep 2026 17:25:50 +0100 Subject: [PATCH 1/4] fix(#641): trim member full name before persistence --- .../postgres/component/MemberMapper.java | 4 ++-- .../postgres/component/MemberMapperTest.java | 23 ++++++++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) 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(), From a932fb528b459d3a2fe81bd95b219e454835bbc6 Mon Sep 17 00:00:00 2001 From: Iryna Lypnyk Date: Mon, 28 Sep 2026 18:19:47 +0100 Subject: [PATCH 2/4] fix(#641): trim member full name in create form --- .../pages/admin/members/create.test.tsx | 75 +++++++++++++++++++ admin-wcc-app/pages/admin/members/create.tsx | 27 ++++--- 2 files changed, 90 insertions(+), 12 deletions(-) create mode 100644 admin-wcc-app/__tests__/pages/admin/members/create.test.tsx 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..609cec69 --- /dev/null +++ b/admin-wcc-app/__tests__/pages/admin/members/create.test.tsx @@ -0,0 +1,75 @@ +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(), + }); + }); + + it('trims full name before submitting member data', async () => { + mockApiFetch.mockResolvedValueOnce({}); + + const user = userEvent.setup(); + + render(); + + 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), 'janedoe'); + + 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); + + 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', + }); + }); +}); diff --git a/admin-wcc-app/pages/admin/members/create.tsx b/admin-wcc-app/pages/admin/members/create.tsx index 20ec792a..f52ed1b0 100644 --- a/admin-wcc-app/pages/admin/members/create.tsx +++ b/admin-wcc-app/pages/admin/members/create.tsx @@ -1,21 +1,21 @@ import { useState } from 'react'; import { - Paper, - Typography, - Button, - TextField, - Box, + Alert, Autocomplete, + Box, + Breadcrumbs, + Button, Chip, - IconButton, - Select, - MenuItem, + CircularProgress, FormControl, + IconButton, InputLabel, - Alert, - CircularProgress, - Breadcrumbs, Link as MuiLink, + MenuItem, + Paper, + Select, + TextField, + Typography, } from '@mui/material'; import Grid from '@mui/material/Grid2'; import DeleteIcon from '@mui/icons-material/Delete'; @@ -127,7 +127,9 @@ 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'; @@ -166,6 +168,7 @@ export default function CreateMemberPage() { const submitData = { ...restFormData, + fullName: restFormData.fullName.trim(), country: { countryCode: country?.countryCode, countryName: country?.countryName, From b44a243d2580333e68557b5faf6cafc00d89ade1 Mon Sep 17 00:00:00 2001 From: Iryna Lypnyk Date: Mon, 28 Sep 2026 22:30:53 +0100 Subject: [PATCH 3/4] chore(#641): remove unrelated import changes --- admin-wcc-app/pages/admin/members/create.tsx | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/admin-wcc-app/pages/admin/members/create.tsx b/admin-wcc-app/pages/admin/members/create.tsx index f52ed1b0..bd2505c8 100644 --- a/admin-wcc-app/pages/admin/members/create.tsx +++ b/admin-wcc-app/pages/admin/members/create.tsx @@ -1,21 +1,21 @@ import { useState } from 'react'; import { - Alert, - Autocomplete, - Box, - Breadcrumbs, + Paper, + Typography, Button, + TextField, + Box, + Autocomplete, Chip, - CircularProgress, - FormControl, IconButton, + Select, + MenuItem, + FormControl, InputLabel, + Alert, + CircularProgress, + Breadcrumbs, Link as MuiLink, - MenuItem, - Paper, - Select, - TextField, - Typography, } from '@mui/material'; import Grid from '@mui/material/Grid2'; import DeleteIcon from '@mui/icons-material/Delete'; From 842ffe6d03963e4f33a1db3cc864f0219ff5f46b Mon Sep 17 00:00:00 2001 From: Iryna Lypnyk Date: Sun, 1 Nov 2026 19:19:13 +0000 Subject: [PATCH 4/4] fix(#641): trim member Slack display name --- .../pages/admin/members/create.test.tsx | 38 +++++++++++++++---- admin-wcc-app/pages/admin/members/create.tsx | 5 ++- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/admin-wcc-app/__tests__/pages/admin/members/create.test.tsx b/admin-wcc-app/__tests__/pages/admin/members/create.test.tsx index 609cec69..305319e1 100644 --- a/admin-wcc-app/__tests__/pages/admin/members/create.test.tsx +++ b/admin-wcc-app/__tests__/pages/admin/members/create.test.tsx @@ -31,17 +31,11 @@ describe('CreateMemberPage', () => { }); }); - it('trims full name before submitting member data', async () => { - mockApiFetch.mockResolvedValueOnce({}); - - const user = userEvent.setup(); - - render(); - + 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), 'janedoe'); + await user.type(screen.getByLabelText(/slack display name/i), ' jane doe '); const countryInput = screen.getByLabelText(/country/i); await user.click(countryInput); @@ -59,6 +53,15 @@ describe('CreateMemberPage', () => { 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 })); @@ -70,6 +73,25 @@ describe('CreateMemberPage', () => { 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 bd2505c8..d1ca2d76 100644 --- a/admin-wcc-app/pages/admin/members/create.tsx +++ b/admin-wcc-app/pages/admin/members/create.tsx @@ -133,7 +133,9 @@ export default function CreateMemberPage() { 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'; @@ -169,6 +171,7 @@ export default function CreateMemberPage() { const submitData = { ...restFormData, fullName: restFormData.fullName.trim(), + slackDisplayName: restFormData.slackDisplayName.trim(), country: { countryCode: country?.countryCode, countryName: country?.countryName,