Skip to content

Commit 29a1ed1

Browse files
authored
AYS-192 | User Detail Flow Has Been Created (#340)
1 parent d46dfe1 commit 29a1ed1

File tree

8 files changed

+331
-1
lines changed

8 files changed

+331
-1
lines changed

src/main/java/org/ays/auth/controller/AysUserController.java

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,21 @@
33
import jakarta.validation.Valid;
44
import lombok.RequiredArgsConstructor;
55
import org.ays.auth.model.AysUser;
6+
import org.ays.auth.model.mapper.AysUserToResponseMapper;
67
import org.ays.auth.model.mapper.AysUserToUsersResponseMapper;
78
import org.ays.auth.model.request.AysUserListRequest;
9+
import org.ays.auth.model.response.AysUserResponse;
810
import org.ays.auth.model.response.AysUsersResponse;
911
import org.ays.auth.service.AysUserReadService;
1012
import org.ays.common.model.AysPage;
1113
import org.ays.common.model.response.AysPageResponse;
1214
import org.ays.common.model.response.AysResponse;
15+
import org.hibernate.validator.constraints.UUID;
1316
import org.springframework.security.access.prepost.PreAuthorize;
17+
import org.springframework.validation.annotation.Validated;
1418
import org.springframework.web.bind.annotation.PostMapping;
19+
import org.springframework.web.bind.annotation.GetMapping;
20+
import org.springframework.web.bind.annotation.PathVariable;
1521
import org.springframework.web.bind.annotation.RequestBody;
1622
import org.springframework.web.bind.annotation.RequestMapping;
1723
import org.springframework.web.bind.annotation.RestController;
@@ -22,6 +28,7 @@
2228
*
2329
* @see AysUserReadService
2430
*/
31+
@Validated
2532
@RestController
2633
@RequiredArgsConstructor
2734
@RequestMapping("/api/v1")
@@ -31,6 +38,7 @@ class AysUserController {
3138

3239

3340
private final AysUserToUsersResponseMapper userToUsersResponseMapper = AysUserToUsersResponseMapper.initialize();
41+
private final AysUserToResponseMapper userToResponseMapper = AysUserToResponseMapper.initialize();
3442

3543

3644
/**
@@ -56,4 +64,25 @@ public AysResponse<AysPageResponse<AysUsersResponse>> findAll(@RequestBody @Vali
5664

5765
return AysResponse.successOf(pageOfUsersResponse);
5866
}
59-
}
67+
68+
69+
/**
70+
* GET /user/{id} : Retrieve the details of a user by its ID.
71+
* <p>
72+
* This endpoint handles the retrieval of a user by its ID. The user must have the 'user:detail'
73+
* authority to access this endpoint.
74+
* </p>
75+
*
76+
* @param id The ID of the user to retrieve.
77+
* @return An {@link AysResponse} containing the {@link AysUserResponse} if the user is found.
78+
*/
79+
80+
@GetMapping("/user/{id}")
81+
@PreAuthorize("hasAuthority('user:detail')")
82+
public AysResponse<AysUserResponse> findById(@PathVariable @UUID final String id) {
83+
final AysUser user = userReadService.findById(id);
84+
final AysUserResponse userResponse = userToResponseMapper.map(user);
85+
return AysResponse.successOf(userResponse);
86+
}
87+
88+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package org.ays.auth.model.mapper;
2+
3+
import org.ays.auth.model.AysUser;
4+
import org.ays.auth.model.response.AysUserResponse;
5+
import org.ays.common.model.mapper.BaseMapper;
6+
import org.mapstruct.Mapper;
7+
import org.mapstruct.factory.Mappers;
8+
9+
/**
10+
* {@link AysUserToResponseMapper} is an interface that defines the mapping between an {@link AysUser} and an {@link AysUserResponse}.
11+
* This interface uses the MapStruct annotation @Mapper to generate an implementation of this interface at compile-time.
12+
* <p>The class provides a static method {@code initialize()} that returns an instance of the generated mapper implementation.
13+
* <p>The interface extends the MapStruct interface {@link BaseMapper}, which defines basic mapping methods.
14+
* The interface adds no additional mapping methods, but simply defines the types to be used in the mapping process.
15+
*/
16+
@Mapper
17+
public interface AysUserToResponseMapper extends BaseMapper<AysUser, AysUserResponse> {
18+
19+
/**
20+
* Initializes the mapper.
21+
*
22+
* @return the initialized mapper object.
23+
*/
24+
static AysUserToResponseMapper initialize() {
25+
return Mappers.getMapper(AysUserToResponseMapper.class);
26+
}
27+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.ays.auth.model.response;
2+
3+
import lombok.Getter;
4+
import lombok.Setter;
5+
import org.ays.auth.model.enums.AysUserStatus;
6+
import org.ays.common.model.AysPhoneNumber;
7+
8+
import java.time.LocalDateTime;
9+
import java.util.List;
10+
11+
@Getter
12+
@Setter
13+
public class AysUserResponse {
14+
15+
private String id;
16+
private String emailAddress;
17+
private String firstName;
18+
private String lastName;
19+
private AysPhoneNumber phoneNumber;
20+
private String city;
21+
private AysUserStatus status;
22+
private List<Role> roles;
23+
private String createdUser;
24+
private LocalDateTime createdAt;
25+
private String updatedUser;
26+
private LocalDateTime updatedAt;
27+
28+
@Getter
29+
@Setter
30+
public static class Role {
31+
private String id;
32+
private String name;
33+
}
34+
}

src/main/java/org/ays/auth/service/AysUserReadService.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,12 @@ public interface AysUserReadService {
3232
*/
3333
AysPage<AysUser> findAll(AysUserListRequest listRequest);
3434

35+
/**
36+
* Retrieves the details of a specific user by its ID.
37+
*
38+
* @param id The ID of the user.
39+
* @return The user with the specified ID, or null if not found.
40+
*/
41+
AysUser findById(String id);
42+
3543
}

src/main/java/org/ays/auth/service/impl/AysUserReadServiceImpl.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import org.ays.auth.model.request.AysUserListRequest;
88
import org.ays.auth.port.AysUserReadPort;
99
import org.ays.auth.service.AysUserReadService;
10+
import org.ays.auth.util.exception.AysUserNotExistByIdException;
1011
import org.ays.common.model.AysPage;
1112
import org.ays.common.model.AysPageable;
1213
import org.springframework.stereotype.Service;
@@ -65,4 +66,20 @@ public AysPage<AysUser> findAll(AysUserListRequest listRequest) {
6566
return userReadPort.findAll(aysPageable, listRequest.getFilter());
6667
}
6768

69+
/**
70+
* Retrieves a user by its unique identifier.
71+
* <p>
72+
* If the user with the specified ID does not exist, an {@link AysUserNotExistByIdException} is thrown.
73+
* </p>
74+
*
75+
* @param id the unique identifier of the user.
76+
* @return the user with the specified ID.
77+
* @throws AysUserNotExistByIdException if the user with the specified ID does not exist.
78+
*/
79+
@Override
80+
public AysUser findById(String id) {
81+
return userReadPort.findById(id)
82+
.orElseThrow(() -> new AysUserNotExistByIdException(id));
83+
}
84+
6885
}

src/test/java/org/ays/auth/controller/AysUserControllerTest.java

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,19 @@
44
import org.ays.AysRestControllerTest;
55
import org.ays.auth.model.AysUser;
66
import org.ays.auth.model.AysUserBuilder;
7+
import org.ays.auth.model.mapper.AysUserToResponseMapper;
78
import org.ays.auth.model.mapper.AysUserToUsersResponseMapper;
89
import org.ays.auth.model.request.AysUserListRequest;
910
import org.ays.auth.model.request.AysUserListRequestBuilder;
11+
import org.ays.auth.model.response.AysUserResponse;
1012
import org.ays.auth.model.response.AysUsersResponse;
1113
import org.ays.auth.service.AysUserReadService;
1214
import org.ays.common.model.AysPage;
1315
import org.ays.common.model.AysPageBuilder;
1416
import org.ays.common.model.response.AysErrorResponse;
1517
import org.ays.common.model.response.AysPageResponse;
1618
import org.ays.common.model.response.AysResponse;
19+
import org.ays.common.util.AysRandomUtil;
1720
import org.ays.common.util.exception.model.AysErrorBuilder;
1821
import org.ays.util.AysMockMvcRequestBuilders;
1922
import org.ays.util.AysMockResultMatchersBuilders;
@@ -33,6 +36,7 @@ class AysUserControllerTest extends AysRestControllerTest {
3336

3437

3538
private final AysUserToUsersResponseMapper userToUsersResponseMapper = AysUserToUsersResponseMapper.initialize();
39+
private final AysUserToResponseMapper userToResponseMapper = AysUserToResponseMapper.initialize();
3640

3741

3842
private static final String BASE_PATH = "/api/v1";
@@ -207,4 +211,90 @@ void givenValidUserListRequest_whenUserUnauthorized_thenReturnAccessDeniedExcept
207211
.findAll(Mockito.any(AysUserListRequest.class));
208212
}
209213

214+
215+
@Test
216+
void givenValidUserId_whenUserFound_thenReturnAysUserResponse() throws Exception {
217+
218+
// Given
219+
String mockUserId = AysRandomUtil.generateUUID();
220+
221+
// When
222+
AysUser mockUser = new AysUserBuilder()
223+
.withValidValues()
224+
.withId(mockUserId)
225+
.build();
226+
227+
Mockito.when(userReadService.findById(mockUserId))
228+
.thenReturn(mockUser);
229+
230+
// Then
231+
String endpoint = BASE_PATH.concat("/user/").concat(mockUserId);
232+
MockHttpServletRequestBuilder mockHttpServletRequestBuilder = AysMockMvcRequestBuilders
233+
.get(endpoint, mockAdminToken.getAccessToken());
234+
235+
AysUserResponse mockUserResponse = userToResponseMapper
236+
.map(mockUser);
237+
AysResponse<AysUserResponse> mockResponse = AysResponse
238+
.successOf(mockUserResponse);
239+
240+
aysMockMvc.perform(mockHttpServletRequestBuilder, mockResponse)
241+
.andExpect(AysMockResultMatchersBuilders.status()
242+
.isOk())
243+
.andExpect(AysMockResultMatchersBuilders.response()
244+
.isNotEmpty());
245+
246+
// Verify
247+
Mockito.verify(userReadService, Mockito.times(1))
248+
.findById(mockUserId);
249+
}
250+
251+
@Test
252+
void givenUserId_whenUnauthorizedForGettingUserById_thenReturnAccessDeniedException() throws Exception {
253+
254+
// Given
255+
String mockUserId = AysRandomUtil.generateUUID();
256+
257+
// Then
258+
String endpoint = BASE_PATH.concat("/user/".concat(mockUserId));
259+
MockHttpServletRequestBuilder mockHttpServletRequestBuilder = AysMockMvcRequestBuilders
260+
.get(endpoint, mockUserToken.getAccessToken());
261+
262+
AysErrorResponse mockErrorResponse = AysErrorBuilder.FORBIDDEN;
263+
264+
aysMockMvc.perform(mockHttpServletRequestBuilder, mockErrorResponse)
265+
.andExpect(AysMockResultMatchersBuilders.status()
266+
.isForbidden())
267+
.andExpect(AysMockResultMatchersBuilders.subErrors()
268+
.doesNotExist());
269+
270+
// Verify
271+
Mockito.verify(userReadService, Mockito.never())
272+
.findById(mockUserId);
273+
}
274+
275+
@ParameterizedTest
276+
@ValueSource(strings = {
277+
"A",
278+
"493268349068342"
279+
})
280+
void givenInvalidId_whenIdNotValid_thenReturnValidationError(String invalidId) throws Exception {
281+
282+
// Then
283+
String endpoint = BASE_PATH.concat("/user/").concat(invalidId);
284+
MockHttpServletRequestBuilder mockHttpServletRequestBuilder = AysMockMvcRequestBuilders
285+
.get(endpoint, mockAdminToken.getAccessToken());
286+
287+
AysErrorResponse mockErrorResponse = AysErrorBuilder.VALIDATION_ERROR;
288+
289+
aysMockMvc.perform(mockHttpServletRequestBuilder, mockErrorResponse)
290+
.andExpect(AysMockResultMatchersBuilders.status()
291+
.isBadRequest())
292+
.andExpect(AysMockResultMatchersBuilders.subErrors()
293+
.isNotEmpty());
294+
295+
// Verify
296+
Mockito.verify(userReadService, Mockito.never())
297+
.findById(Mockito.anyString());
298+
}
299+
210300
}

src/test/java/org/ays/auth/controller/AysUserEndToEndTest.java

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22

33
import org.ays.AysEndToEndTest;
44
import org.ays.auth.model.AysRole;
5+
import org.ays.auth.model.AysUser;
56
import org.ays.auth.model.AysUserBuilder;
67
import org.ays.auth.model.enums.AysUserStatus;
8+
import org.ays.auth.model.mapper.AysUserToResponseMapper;
79
import org.ays.auth.model.request.AysUserListRequest;
810
import org.ays.auth.model.request.AysUserListRequestBuilder;
11+
import org.ays.auth.model.response.AysUserResponse;
912
import org.ays.auth.model.response.AysUsersResponse;
1013
import org.ays.auth.port.AysRoleReadPort;
1114
import org.ays.auth.port.AysUserSavePort;
@@ -14,6 +17,7 @@
1417
import org.ays.common.model.response.AysResponseBuilder;
1518
import org.ays.institution.model.Institution;
1619
import org.ays.institution.model.InstitutionBuilder;
20+
import org.ays.institution.port.InstitutionSavePort;
1721
import org.ays.util.AysMockMvcRequestBuilders;
1822
import org.ays.util.AysMockResultMatchersBuilders;
1923
import org.ays.util.AysValidTestData;
@@ -32,6 +36,12 @@ class AysUserEndToEndTest extends AysEndToEndTest {
3236
@Autowired
3337
private AysRoleReadPort roleReadPort;
3438

39+
@Autowired
40+
private InstitutionSavePort institutionSavePort;
41+
42+
43+
private final AysUserToResponseMapper userToResponseMapper = AysUserToResponseMapper.initialize();
44+
3545

3646
private static final String BASE_PATH = "/api/v1";
3747

@@ -154,4 +164,70 @@ void givenValidUserListRequest_whenUsersFoundForAdmin_thenReturnAysPageResponseO
154164
.isEmpty());
155165
}
156166

167+
168+
@Test
169+
void givenValidUserId_whenUserExists_thenReturnAysUserResponse() throws Exception {
170+
171+
// Initialize
172+
Institution institution = institutionSavePort.save(
173+
new InstitutionBuilder()
174+
.withValidValues()
175+
.withoutId()
176+
.build()
177+
);
178+
List<AysRole> roles = roleReadPort.findAll();
179+
AysUser user = userSavePort.save(
180+
new AysUserBuilder()
181+
.withValidValues()
182+
.withoutId()
183+
.withRoles(roles)
184+
.withInstitution(institution)
185+
.build()
186+
);
187+
188+
// Given
189+
String userId = user.getId();
190+
191+
// Then
192+
String endpoint = BASE_PATH.concat("/user/").concat(userId);
193+
MockHttpServletRequestBuilder mockHttpServletRequestBuilder = AysMockMvcRequestBuilders
194+
.get(endpoint, adminToken.getAccessToken());
195+
196+
AysUserResponse mockUserResponse = userToResponseMapper
197+
.map(user);
198+
199+
AysResponse<AysUserResponse> mockResponse = AysResponse
200+
.successOf(mockUserResponse);
201+
202+
aysMockMvc.perform(mockHttpServletRequestBuilder, mockResponse)
203+
.andExpect(AysMockResultMatchersBuilders.status()
204+
.isOk())
205+
.andExpect(AysMockResultMatchersBuilders.response()
206+
.isNotEmpty())
207+
.andExpect(AysMockResultMatchersBuilders.response("id")
208+
.value(user.getId()))
209+
.andExpect(AysMockResultMatchersBuilders.response("emailAddress")
210+
.value(user.getEmailAddress()))
211+
.andExpect(AysMockResultMatchersBuilders.response("firstName")
212+
.value(user.getFirstName()))
213+
.andExpect(AysMockResultMatchersBuilders.response("lastName")
214+
.value(user.getLastName()))
215+
.andExpect(AysMockResultMatchersBuilders.response("phoneNumber.countryCode")
216+
.isNotEmpty())
217+
.andExpect(AysMockResultMatchersBuilders.response("phoneNumber.lineNumber")
218+
.isNotEmpty())
219+
.andExpect(AysMockResultMatchersBuilders.response("city")
220+
.value(user.getCity()))
221+
.andExpect(AysMockResultMatchersBuilders.response("status")
222+
.isNotEmpty())
223+
.andExpect(AysMockResultMatchersBuilders.response("roles[*].id")
224+
.isNotEmpty())
225+
.andExpect(AysMockResultMatchersBuilders.response("roles[*].name")
226+
.isNotEmpty())
227+
.andExpect(AysMockResultMatchersBuilders.response("createdUser")
228+
.value(user.getCreatedUser()))
229+
.andExpect(AysMockResultMatchersBuilders.response("createdAt")
230+
.isNotEmpty());
231+
}
232+
157233
}

0 commit comments

Comments
 (0)