diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 92c3618..b53bbd8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -25,5 +25,5 @@ jobs:
distribution: temurin
cache: maven
- - name: Run tests
- run: mvn clean test --batch-mode --no-transfer-progress
+ - name: Build and Test
+ run: mvn clean verify --batch-mode --no-transfer-progress
diff --git a/pom.xml b/pom.xml
index 9e13819..c3e303e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -116,6 +116,12 @@
zipkin-reporter-brave
+
+
+ org.springframework.boot
+ spring-boot-starter-aop
+
+
org.springframework.boot
diff --git a/src/main/java/com/cn/hotelDemo/annotation/AuditLogged.java b/src/main/java/com/cn/hotelDemo/annotation/AuditLogged.java
new file mode 100644
index 0000000..46d9a67
--- /dev/null
+++ b/src/main/java/com/cn/hotelDemo/annotation/AuditLogged.java
@@ -0,0 +1,14 @@
+package com.cn.hotelDemo.annotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface AuditLogged {
+ String action();
+ String resourceType();
+ String resourceIdSpel() default "''";
+}
diff --git a/src/main/java/com/cn/hotelDemo/aspect/AuditAspect.java b/src/main/java/com/cn/hotelDemo/aspect/AuditAspect.java
new file mode 100644
index 0000000..9661dec
--- /dev/null
+++ b/src/main/java/com/cn/hotelDemo/aspect/AuditAspect.java
@@ -0,0 +1,76 @@
+package com.cn.hotelDemo.aspect;
+
+import java.lang.reflect.Method;
+
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.ExpressionParser;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.stereotype.Component;
+
+import com.cn.hotelDemo.annotation.AuditLogged;
+import com.cn.hotelDemo.service.AuditService;
+
+@Aspect
+@Component
+public class AuditAspect {
+
+ private final AuditService auditService;
+ private final ExpressionParser parser = new SpelExpressionParser();
+
+ public AuditAspect(AuditService auditService) {
+ this.auditService = auditService;
+ }
+
+ @AfterReturning(pointcut = "@annotation(auditLogged)", returning = "result")
+ public void logAudit(JoinPoint joinPoint, AuditLogged auditLogged, Object result) {
+ Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ String username = (authentication != null && authentication.getName() != null) ? authentication.getName() : "anonymous";
+
+ String resourceId = resolveSpelExpression(joinPoint, result, auditLogged.resourceIdSpel(), "N/A");
+ String action = auditLogged.action().startsWith("#") ? resolveSpelExpression(joinPoint, result, auditLogged.action(), "UNKNOWN") : auditLogged.action();
+
+ auditService.record(
+ action,
+ username,
+ auditLogged.resourceType(),
+ resourceId,
+ "SUCCESS",
+ "Action performed successfully"
+ );
+ }
+
+ private String resolveSpelExpression(JoinPoint joinPoint, Object result, String spelExpression, String defaultValue) {
+ if (spelExpression == null || spelExpression.isEmpty() || spelExpression.equals("''")) {
+ return defaultValue;
+ }
+
+ EvaluationContext context = new StandardEvaluationContext();
+
+ // Add method arguments to context
+ MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+ String[] parameterNames = signature.getParameterNames();
+ Object[] args = joinPoint.getArgs();
+ if (parameterNames != null) {
+ for (int i = 0; i < parameterNames.length; i++) {
+ context.setVariable(parameterNames[i], args[i]);
+ }
+ }
+
+ // Add method return value to context
+ context.setVariable("result", result);
+
+ try {
+ Object evaluatedId = parser.parseExpression(spelExpression).getValue(context);
+ return evaluatedId != null ? String.valueOf(evaluatedId) : "N/A";
+ } catch (Exception e) {
+ return "UNKNOWN";
+ }
+ }
+}
diff --git a/src/main/java/com/cn/hotelDemo/controller/BookingController.java b/src/main/java/com/cn/hotelDemo/controller/BookingController.java
index b3b2e55..2d54556 100644
--- a/src/main/java/com/cn/hotelDemo/controller/BookingController.java
+++ b/src/main/java/com/cn/hotelDemo/controller/BookingController.java
@@ -18,7 +18,7 @@
import com.cn.hotelDemo.dto.BookingResponse;
import com.cn.hotelDemo.model.Booking;
import com.cn.hotelDemo.model.BookingStatus;
-import com.cn.hotelDemo.service.AuditService;
+import com.cn.hotelDemo.annotation.AuditLogged;
import com.cn.hotelDemo.service.BookingService;
import io.swagger.v3.oas.annotations.Operation;
@@ -33,21 +33,17 @@
public class BookingController {
private final BookingService bookingService;
- private final AuditService auditService;
-
- public BookingController(BookingService bookingService, AuditService auditService) {
+ public BookingController(BookingService bookingService) {
this.bookingService = bookingService;
- this.auditService = auditService;
}
@PostMapping("/create")
@PreAuthorize("hasRole('ADMIN') or hasAuthority('admin') or hasRole('NORMAL') or hasAuthority('normal')")
@Operation(summary = "Create a new booking")
+ @AuditLogged(action = "#result != null ? 'BOOKING_' + #result.body.status.name() : 'UNKNOWN'", resourceType = "BOOKING", resourceIdSpel = "#result.body.bookingId")
public ResponseEntity createBooking(@Valid @RequestBody BookingRequest bookingRequest,
Authentication authentication) {
BookingResponse response = bookingService.createBooking(bookingRequest);
- auditService.record("BOOKING_" + response.getStatus().name(), authentication.getName(), "BOOKING",
- String.valueOf(response.getBookingId()), response.getStatus().name(), response.getMessage());
return new ResponseEntity<>(response,
response.getStatus() == BookingStatus.CONFIRMED ? HttpStatus.CREATED : HttpStatus.CONFLICT);
}
@@ -57,9 +53,6 @@ public ResponseEntity createBooking(@Valid @RequestBody Booking
@Operation(summary = "Get a booking by its ID")
public ResponseEntity getBookingById(@PathVariable Long id) {
Booking booking = bookingService.getBookingById(id);
- if (booking == null) {
- return ResponseEntity.notFound().build();
- }
return ResponseEntity.ok(booking);
}
@@ -87,11 +80,9 @@ public ResponseEntity> getBookingsByHotel(@PathVariable Long hotel
@PostMapping("/cancel/{id}")
@PreAuthorize("hasRole('ADMIN') or hasAuthority('admin') or hasRole('NORMAL') or hasAuthority('normal')")
@Operation(summary = "Cancel a booking by its ID")
+ @AuditLogged(action = "BOOKING_CANCELLED", resourceType = "BOOKING", resourceIdSpel = "#result.body.bookingId")
public ResponseEntity cancelBooking(@PathVariable Long id, Authentication authentication) {
Booking booking = bookingService.getBookingById(id);
- if (booking == null) {
- return ResponseEntity.notFound().build();
- }
boolean isAdmin = authentication.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN") || a.getAuthority().equals("admin"));
@@ -102,8 +93,6 @@ public ResponseEntity cancelBooking(@PathVariable Long id, Auth
}
BookingResponse response = bookingService.cancelBooking(id);
- auditService.record("BOOKING_CANCELLED", authentication.getName(), "BOOKING",
- String.valueOf(response.getBookingId()), response.getStatus().name(), response.getMessage());
return ResponseEntity.ok(response);
}
}
diff --git a/src/main/java/com/cn/hotelDemo/controller/HotelController.java b/src/main/java/com/cn/hotelDemo/controller/HotelController.java
index 6b14730..fe90b6c 100644
--- a/src/main/java/com/cn/hotelDemo/controller/HotelController.java
+++ b/src/main/java/com/cn/hotelDemo/controller/HotelController.java
@@ -20,7 +20,7 @@
import com.cn.hotelDemo.dto.HotelRequest;
import com.cn.hotelDemo.model.Hotel;
import com.cn.hotelDemo.service.HotelService;
-import com.cn.hotelDemo.service.AuditService;
+import com.cn.hotelDemo.annotation.AuditLogged;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
@@ -34,29 +34,22 @@ public class HotelController {
@Autowired
HotelService hotelService;
-
- @Autowired
- AuditService auditService;
@GetMapping("/userDetail")
@Operation(summary = "Get current authenticated user details from OIDC")
+ @AuditLogged(action = "OIDC_PROFILE_VIEWED", resourceType = "AUTH", resourceIdSpel = "#oidcUser.email")
public String getDetails(@AuthenticationPrincipal OidcUser oidcUser) {
- String details = "User name: %s, email: %s".formatted(oidcUser.getFullName(), oidcUser.getEmail());
- auditService.record("OIDC_PROFILE_VIEWED", oidcUser.getEmail(), "AUTH", oidcUser.getEmail(), "SUCCESS",
- "Authenticated profile details were requested");
- return details;
+ return "User name: %s, email: %s".formatted(oidcUser.getFullName(), oidcUser.getEmail());
}
@PostMapping("/create")
@PreAuthorize("hasRole('ADMIN') or hasAuthority('admin')")
@Operation(summary = "Create a new hotel (Admin only)")
- public void createHotel(@Valid @RequestBody HotelRequest hotelRequest, Authentication authentication)
+ @AuditLogged(action = "HOTEL_CREATED", resourceType = "HOTEL", resourceIdSpel = "#result.id")
+ public Hotel createHotel(@Valid @RequestBody HotelRequest hotelRequest, Authentication authentication)
{
- Hotel createdHotel = hotelService.createHotel(hotelRequest);
- auditService.record("HOTEL_CREATED", authentication.getName(), "HOTEL", String.valueOf(createdHotel.getId()),
- "SUCCESS", "Hotel created with name=%s, city=%s".formatted(createdHotel.getName(),
- createdHotel.getCity()));
- }
+ return hotelService.createHotel(hotelRequest);
+ }
@GetMapping("/id/{id}")
@PreAuthorize("hasRole('NORMAL') or hasAuthority('normal')")
@@ -77,11 +70,9 @@ public List getAllHotels()
@DeleteMapping("/remove/id/{id}")
@PreAuthorize("hasRole('admin') or hasAuthority('admin')")
@Operation(summary = "Delete a hotel by ID (Admin only)")
+ @AuditLogged(action = "HOTEL_DELETED", resourceType = "HOTEL", resourceIdSpel = "#id")
public void deleteHotelById(@PathVariable Long id, Authentication authentication)
{
hotelService.deleteHotelById(id);
- auditService.record("HOTEL_DELETED", authentication.getName(), "HOTEL", String.valueOf(id), "SUCCESS",
- "Hotel delete requested for id=%s".formatted(id));
-
}
}
diff --git a/src/main/java/com/cn/hotelDemo/controller/LoginController.java b/src/main/java/com/cn/hotelDemo/controller/LoginController.java
index a5cbc26..ce4e1e1 100644
--- a/src/main/java/com/cn/hotelDemo/controller/LoginController.java
+++ b/src/main/java/com/cn/hotelDemo/controller/LoginController.java
@@ -5,7 +5,7 @@
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
-import com.cn.hotelDemo.service.AuditService;
+import com.cn.hotelDemo.annotation.AuditLogged;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -14,17 +14,13 @@
@Tag(name = "Login Controller", description = "Endpoints for authentication UI")
public class LoginController {
- private final AuditService auditService;
-
- public LoginController(AuditService auditService) {
- this.auditService = auditService;
+ public LoginController() {
}
@GetMapping("/login")
@Operation(summary = "Serve the login page")
+ @AuditLogged(action = "LOGIN_PAGE_VIEWED", resourceType = "AUTH", resourceIdSpel = "#request.getRemoteAddr()")
public String login(HttpServletRequest request) {
- auditService.record("LOGIN_PAGE_VIEWED", "anonymous", "AUTH", "/login", "SUCCESS",
- "Login page rendered from %s".formatted(request.getRemoteAddr()));
return "login";
}
}
diff --git a/src/main/java/com/cn/hotelDemo/controller/RoomController.java b/src/main/java/com/cn/hotelDemo/controller/RoomController.java
index d4dad5f..c43a2cd 100644
--- a/src/main/java/com/cn/hotelDemo/controller/RoomController.java
+++ b/src/main/java/com/cn/hotelDemo/controller/RoomController.java
@@ -19,7 +19,7 @@
import com.cn.hotelDemo.dto.RoomRequest;
import com.cn.hotelDemo.model.Room;
-import com.cn.hotelDemo.service.AuditService;
+import com.cn.hotelDemo.annotation.AuditLogged;
import com.cn.hotelDemo.service.RoomService;
import jakarta.validation.Valid;
@@ -35,21 +35,16 @@
public class RoomController {
private final RoomService roomService;
- private final AuditService auditService;
-
- public RoomController(RoomService roomService, AuditService auditService) {
+ public RoomController(RoomService roomService) {
this.roomService = roomService;
- this.auditService = auditService;
}
@PostMapping("/create")
@PreAuthorize("hasRole('ADMIN') or hasAuthority('admin')")
@Operation(summary = "Create a new room in a hotel")
+ @AuditLogged(action = "ROOM_CREATED", resourceType = "ROOM", resourceIdSpel = "#result.body.id")
public ResponseEntity createRoom(@Valid @RequestBody RoomRequest roomRequest, Authentication authentication) {
Room room = roomService.createRoom(roomRequest);
- auditService.record("ROOM_CREATED", authentication.getName(), "ROOM", String.valueOf(room.getId()), "SUCCESS",
- "Room created for hotelId=%s, roomNumber=%s".formatted(roomRequest.getHotelId(),
- roomRequest.getRoomNumber()));
return new ResponseEntity<>(room, HttpStatus.CREATED);
}
@@ -63,15 +58,13 @@ public ResponseEntity> getRoomsByHotel(@PathVariable Long hotelId) {
@GetMapping("/hotel/{hotelId}/available")
@PreAuthorize("hasRole('ADMIN') or hasAuthority('admin') or hasRole('NORMAL') or hasAuthority('normal')")
@Operation(summary = "Check room availability between dates for a hotel")
+ @AuditLogged(action = "ROOM_AVAILABILITY_SEARCHED", resourceType = "ROOM", resourceIdSpel = "#hotelId")
public ResponseEntity> getAvailableRoomsByHotel(
@PathVariable Long hotelId,
Authentication authentication,
@RequestParam @DateTimeFormat(iso = ISO.DATE) LocalDate checkInDate,
@RequestParam @DateTimeFormat(iso = ISO.DATE) LocalDate checkOutDate) {
List rooms = roomService.getAvailableRoomsByHotelAndDates(hotelId, checkInDate, checkOutDate);
- auditService.record("ROOM_AVAILABILITY_SEARCHED", authentication.getName(), "ROOM", String.valueOf(hotelId),
- "SUCCESS", "Availability checked for hotelId=%s between %s and %s".formatted(hotelId, checkInDate,
- checkOutDate));
return ResponseEntity.ok(rooms);
}
@@ -80,9 +73,6 @@ public ResponseEntity> getAvailableRoomsByHotel(
@Operation(summary = "Get a room by its ID")
public ResponseEntity getRoomById(@PathVariable Long id) {
Room room = roomService.getRoomById(id);
- if (room == null) {
- return ResponseEntity.notFound().build();
- }
return ResponseEntity.ok(room);
}
diff --git a/src/main/java/com/cn/hotelDemo/controller/UserController.java b/src/main/java/com/cn/hotelDemo/controller/UserController.java
index 2feb024..bfdacdd 100644
--- a/src/main/java/com/cn/hotelDemo/controller/UserController.java
+++ b/src/main/java/com/cn/hotelDemo/controller/UserController.java
@@ -15,7 +15,7 @@
import com.cn.hotelDemo.dto.UserRequest;
import com.cn.hotelDemo.model.User;
-import com.cn.hotelDemo.service.AuditService;
+import com.cn.hotelDemo.annotation.AuditLogged;
import com.cn.hotelDemo.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
@@ -26,13 +26,9 @@
@RequestMapping("/user")
@Tag(name = "User Controller", description = "Endpoints for managing users")
public class UserController {
-
@Autowired
UserService userService;
- @Autowired
- AuditService auditService;
-
@GetMapping("/getUsers")
@Operation(summary = "Get all users")
@SecurityRequirement(name = "Bearer Authentication")
@@ -49,21 +45,18 @@ public User getUserById(@PathVariable Long id) {
@PostMapping("/createUser")
@Operation(summary = "Register a new user (Public)")
- public void createUser(@Valid @RequestBody UserRequest userRequest)
+ @AuditLogged(action = "USER_CREATED", resourceType = "USER", resourceIdSpel = "#result.id")
+ public User createUser(@Valid @RequestBody UserRequest userRequest)
{
- User createdUser = userService.createUser(userRequest);
- auditService.record("USER_CREATED", userRequest.getUsername(), "USER", String.valueOf(createdUser.getId()),
- "SUCCESS", "Public user registration completed for email=%s".formatted(userRequest.getEmail()));
+ return userService.createUser(userRequest);
}
@DeleteMapping("/remove/id/{id}")
@Operation(summary = "Delete a user by ID")
@SecurityRequirement(name = "Bearer Authentication")
+ @AuditLogged(action = "USER_DELETED", resourceType = "USER", resourceIdSpel = "#id")
public void deleteUserById(@PathVariable Long id)
{
userService.deleteUserById(id);
- auditService.record("USER_DELETED", "system", "USER", String.valueOf(id), "SUCCESS",
- "User delete requested for id=%s".formatted(id));
-
}
}
diff --git a/src/main/java/com/cn/hotelDemo/exception/BookingNotFoundException.java b/src/main/java/com/cn/hotelDemo/exception/BookingNotFoundException.java
new file mode 100644
index 0000000..e35f51e
--- /dev/null
+++ b/src/main/java/com/cn/hotelDemo/exception/BookingNotFoundException.java
@@ -0,0 +1,7 @@
+package com.cn.hotelDemo.exception;
+
+public class BookingNotFoundException extends RuntimeException {
+ public BookingNotFoundException(Long id) {
+ super("Booking not found with ID: " + id);
+ }
+}
diff --git a/src/main/java/com/cn/hotelDemo/exception/GlobalExceptionHandler.java b/src/main/java/com/cn/hotelDemo/exception/GlobalExceptionHandler.java
index 244fab8..fdac143 100644
--- a/src/main/java/com/cn/hotelDemo/exception/GlobalExceptionHandler.java
+++ b/src/main/java/com/cn/hotelDemo/exception/GlobalExceptionHandler.java
@@ -45,6 +45,34 @@ public ResponseEntity handleUserNotFound(UserNotFoundException ex, Htt
return new ResponseEntity<>(error, status);
}
+ @ExceptionHandler(RoomNotFoundException.class)
+ @ResponseBody
+ public ResponseEntity handleRoomNotFound(RoomNotFoundException ex, HttpServletRequest request) {
+ HttpStatus status = HttpStatus.NOT_FOUND;
+ ApiError error = new ApiError(
+ LocalDateTime.now(),
+ status.value(),
+ "Not Found",
+ ex.getMessage(),
+ request.getRequestURI()
+ );
+ return new ResponseEntity<>(error, status);
+ }
+
+ @ExceptionHandler(BookingNotFoundException.class)
+ @ResponseBody
+ public ResponseEntity handleBookingNotFound(BookingNotFoundException ex, HttpServletRequest request) {
+ HttpStatus status = HttpStatus.NOT_FOUND;
+ ApiError error = new ApiError(
+ LocalDateTime.now(),
+ status.value(),
+ "Not Found",
+ ex.getMessage(),
+ request.getRequestURI()
+ );
+ return new ResponseEntity<>(error, status);
+ }
+
@ExceptionHandler(IllegalArgumentException.class)
@ResponseBody
public ResponseEntity handleIllegalArgument(IllegalArgumentException ex, HttpServletRequest request) {
diff --git a/src/main/java/com/cn/hotelDemo/exception/RoomNotFoundException.java b/src/main/java/com/cn/hotelDemo/exception/RoomNotFoundException.java
new file mode 100644
index 0000000..4e46ae9
--- /dev/null
+++ b/src/main/java/com/cn/hotelDemo/exception/RoomNotFoundException.java
@@ -0,0 +1,7 @@
+package com.cn.hotelDemo.exception;
+
+public class RoomNotFoundException extends RuntimeException {
+ public RoomNotFoundException(Long id) {
+ super("Room not found with ID: " + id);
+ }
+}
diff --git a/src/main/java/com/cn/hotelDemo/service/BookingService.java b/src/main/java/com/cn/hotelDemo/service/BookingService.java
index 127fd12..e59b820 100644
--- a/src/main/java/com/cn/hotelDemo/service/BookingService.java
+++ b/src/main/java/com/cn/hotelDemo/service/BookingService.java
@@ -89,7 +89,7 @@ public List getAllBookings() {
}
public Booking getBookingById(Long id) {
- return bookingRepository.findById(id).orElse(null);
+ return bookingRepository.findById(id).orElseThrow(() -> new com.cn.hotelDemo.exception.BookingNotFoundException(id));
}
public List getBookingsByUserId(Long userId) {
diff --git a/src/main/java/com/cn/hotelDemo/service/RoomService.java b/src/main/java/com/cn/hotelDemo/service/RoomService.java
index f60adef..1b08ea7 100644
--- a/src/main/java/com/cn/hotelDemo/service/RoomService.java
+++ b/src/main/java/com/cn/hotelDemo/service/RoomService.java
@@ -79,7 +79,7 @@ public List getRoomsByHotelId(Long hotelId) {
}
public Room getRoomById(Long id) {
- return roomRepository.findById(id).orElse(null);
+ return roomRepository.findById(id).orElseThrow(() -> new com.cn.hotelDemo.exception.RoomNotFoundException(id));
}
public List getAllRooms() {
diff --git a/src/test/java/com/cn/hotelDemo/config/HotelSecurityConfigTest.java b/src/test/java/com/cn/hotelDemo/config/HotelSecurityConfigTest.java
index 5480d70..6b1632c 100644
--- a/src/test/java/com/cn/hotelDemo/config/HotelSecurityConfigTest.java
+++ b/src/test/java/com/cn/hotelDemo/config/HotelSecurityConfigTest.java
@@ -10,6 +10,29 @@
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
+import org.springframework.security.oauth2.core.oidc.OidcIdToken;
+import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority;
+import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
+import org.springframework.core.convert.converter.Converter;
+
import com.cn.hotelDemo.controller.HotelController;
import com.cn.hotelDemo.controller.AuditController;
import com.cn.hotelDemo.controller.LoginController;
@@ -19,9 +42,8 @@
import com.cn.hotelDemo.service.UserService;
import org.springframework.context.annotation.Import;
-
-import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import com.nimbusds.jose.shaded.gson.internal.LinkedTreeMap;
+import com.cn.hotelDemo.model.User;
@WebMvcTest({HotelController.class, AuditController.class, LoginController.class})
@Import(HotelSecurityConfig.class)
@@ -79,4 +101,73 @@ public void loginEndpoint_shouldBePermittedWithoutAuth() throws Exception {
mockMvc.perform(get("/login"))
.andExpect(status().isOk());
}
+
+ @Test
+ public void testJwtAuthenticationConverter_WithRealmAccessRoles() {
+ HotelSecurityConfig config = new HotelSecurityConfig(userRepository, null);
+ JwtAuthenticationConverter converter = config.jwtAuthenticationConverter();
+
+ Map claims = new HashMap<>();
+ LinkedTreeMap> realmAccess = new LinkedTreeMap<>();
+ realmAccess.put("roles", List.of("admin", "user"));
+ claims.put("realm_access", realmAccess);
+
+ Jwt jwt = mock(Jwt.class);
+ when(jwt.getClaims()).thenReturn(claims);
+
+ @SuppressWarnings("unchecked")
+ Converter> authoritiesConverter =
+ (Converter>) (Object)
+ org.springframework.test.util.ReflectionTestUtils.getField(converter, "jwtGrantedAuthoritiesConverter");
+
+ Collection authorities = authoritiesConverter.convert(jwt);
+
+ assertEquals(2, authorities.size());
+ assertTrue(authorities.contains(new SimpleGrantedAuthority("admin")));
+ assertTrue(authorities.contains(new SimpleGrantedAuthority("user")));
+ }
+
+ @Test
+ public void testUserAuthoritiesMapper_WithOAuth2UserAuthority() {
+ HotelSecurityConfig config = new HotelSecurityConfig(userRepository, null);
+ GrantedAuthoritiesMapper mapper = config.userAuthoritiesMapper();
+
+ Map attributes = new HashMap<>();
+ Map realmAccess = new HashMap<>();
+ realmAccess.put("roles", List.of("admin", "manager"));
+ attributes.put("realm_access", realmAccess);
+
+ OAuth2UserAuthority authority = new OAuth2UserAuthority(attributes);
+
+ Collection extends GrantedAuthority> mappedAuthorities = mapper.mapAuthorities(Set.of(authority));
+
+ assertEquals(2, mappedAuthorities.size());
+ assertTrue(mappedAuthorities.contains(new SimpleGrantedAuthority("ROLE_ADMIN")));
+ assertTrue(mappedAuthorities.contains(new SimpleGrantedAuthority("ROLE_MANAGER")));
+ }
+
+ @Test
+ public void testUserAuthoritiesMapper_WithOidcUserAuthority_KnownUser() throws Exception {
+ HotelSecurityConfig config = new HotelSecurityConfig(userRepository, null);
+ GrantedAuthoritiesMapper mapper = config.userAuthoritiesMapper();
+
+ Map attributes = new HashMap<>();
+ attributes.put("email", "test@example.com");
+
+ OidcIdToken idToken = mock(OidcIdToken.class);
+ when(idToken.getIssuer()).thenReturn(java.net.URI.create("https://accounts.google.com").toURL());
+
+ OidcUserAuthority mockAuthority = mock(OidcUserAuthority.class);
+ when(mockAuthority.getAttributes()).thenReturn(attributes);
+ when(mockAuthority.getIdToken()).thenReturn(idToken);
+
+ User dbUser = new User();
+ dbUser.setRole("ADMIN");
+ when(userRepository.findByEmail("test@example.com")).thenReturn(dbUser);
+
+ Collection extends GrantedAuthority> mappedAuthorities = mapper.mapAuthorities(Set.of(mockAuthority));
+
+ assertEquals(1, mappedAuthorities.size());
+ assertTrue(mappedAuthorities.contains(new SimpleGrantedAuthority("ROLE_ADMIN")));
+ }
}