收口预约只读接口权限与酒店隔离
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
package cn.nianxx.thhotel.platform.message.control;
|
||||
|
||||
import cn.nianxx.thhotel.platform.access.common.enums.PlatformPermissionCode;
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextService;
|
||||
import cn.nianxx.thhotel.platform.message.common.result.SourceMessagePageResult;
|
||||
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageConversationResult;
|
||||
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageOriginalResponse;
|
||||
import cn.nianxx.thhotel.platform.message.common.request.SourceMessageOriginalAccessRequest;
|
||||
import cn.nianxx.thhotel.platform.message.common.request.SourceMessageQueryRequest;
|
||||
import cn.nianxx.thhotel.platform.security.service.FrontendAuthorizationService;
|
||||
import cn.nianxx.thhotel.platform.message.service.SourceMessageConversationService;
|
||||
import cn.nianxx.thhotel.platform.message.service.SourceMessageOriginalService;
|
||||
import cn.nianxx.thhotel.platform.message.service.SourceMessageQueryService;
|
||||
@@ -28,6 +31,8 @@ public class SourceMessageController {
|
||||
private final SourceMessageQueryService queryService;
|
||||
private final SourceMessageOriginalService originalService;
|
||||
private final SourceMessageConversationService conversationService;
|
||||
private final FrontendAuthorizationService authorizationService;
|
||||
private final HotelContextService hotelContextService;
|
||||
|
||||
/**
|
||||
* 注入 SourceMessage 查询与原文读取服务,Controller 不直接访问 Mapper 或 Repository。
|
||||
@@ -35,10 +40,14 @@ public class SourceMessageController {
|
||||
public SourceMessageController(
|
||||
SourceMessageQueryService queryService,
|
||||
SourceMessageOriginalService originalService,
|
||||
SourceMessageConversationService conversationService) {
|
||||
SourceMessageConversationService conversationService,
|
||||
FrontendAuthorizationService authorizationService,
|
||||
HotelContextService hotelContextService) {
|
||||
this.queryService = queryService;
|
||||
this.originalService = originalService;
|
||||
this.conversationService = conversationService;
|
||||
this.authorizationService = authorizationService;
|
||||
this.hotelContextService = hotelContextService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,6 +61,7 @@ public class SourceMessageController {
|
||||
@RequestParam(required = false) String captureStatus,
|
||||
@RequestParam(required = false) Integer pageNum,
|
||||
@RequestParam(required = false) Integer pageSize) {
|
||||
authorizationService.requirePermission(PlatformPermissionCode.SOURCE_MESSAGE_READ.name());
|
||||
return queryService.query(new SourceMessageQueryRequest(
|
||||
hotelId,
|
||||
externalMessageId,
|
||||
@@ -67,8 +77,11 @@ public class SourceMessageController {
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public SourceMessageSummaryResponse detail(@PathVariable Long id) {
|
||||
return queryService.getSummary(id)
|
||||
authorizationService.requirePermission(PlatformPermissionCode.SOURCE_MESSAGE_READ.name());
|
||||
SourceMessageSummaryResponse summary = queryService.getSummary(id)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "source message not found"));
|
||||
requireSourceMessageHotelAccess(summary);
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,4 +118,11 @@ public class SourceMessageController {
|
||||
private boolean hasText(String value) {
|
||||
return value != null && !value.trim().isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 SourceMessage 实际归属酒店校验当前用户访问权,避免跨酒店按 ID 读取摘要。
|
||||
*/
|
||||
private void requireSourceMessageHotelAccess(SourceMessageSummaryResponse summary) {
|
||||
hotelContextService.requireAccessibleHotel(summary.hotelId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package cn.nianxx.thhotel.platform.message.control;
|
||||
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextException;
|
||||
import cn.nianxx.thhotel.platform.security.service.impl.FrontendAuthorizationException;
|
||||
import java.util.Map;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
@@ -22,4 +23,16 @@ public class SourceMessageControllerAdvice {
|
||||
"error_code", exception.getErrorCode(),
|
||||
"message", exception.getMessage()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SourceMessage 摘要接口登录或权限不足异常,避免泄漏内部鉴权细节。
|
||||
*/
|
||||
@ExceptionHandler(FrontendAuthorizationException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleFrontendAuthorizationException(
|
||||
FrontendAuthorizationException exception) {
|
||||
return ResponseEntity.status(exception.getStatus())
|
||||
.body(Map.of(
|
||||
"error_code", exception.getErrorCode(),
|
||||
"message", exception.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package cn.nianxx.thhotel.platform.security.service;
|
||||
|
||||
import cn.nianxx.thhotel.platform.security.common.dto.AuthenticatedUserContext;
|
||||
|
||||
/**
|
||||
* 前端业务接口强制鉴权服务。用于普通业务页面接口的登录和权限边界,不复用管理后台错误码。
|
||||
*/
|
||||
public interface FrontendAuthorizationService {
|
||||
|
||||
/**
|
||||
* 要求当前请求已登录,并返回当前用户上下文。
|
||||
*/
|
||||
AuthenticatedUserContext requireLogin();
|
||||
|
||||
/**
|
||||
* 要求当前请求用户拥有指定业务权限码。
|
||||
*/
|
||||
AuthenticatedUserContext requirePermission(String permissionCode);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package cn.nianxx.thhotel.platform.security.service.impl;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
/**
|
||||
* 前端业务接口鉴权受控异常。ControllerAdvice 负责转换为稳定 HTTP 响应。
|
||||
*/
|
||||
public class FrontendAuthorizationException extends RuntimeException {
|
||||
|
||||
private final HttpStatus status;
|
||||
private final String errorCode;
|
||||
|
||||
public FrontendAuthorizationException(HttpStatus status, String errorCode, String message) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public HttpStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package cn.nianxx.thhotel.platform.security.service.impl;
|
||||
|
||||
import cn.nianxx.thhotel.platform.security.common.dto.AuthenticatedUserContext;
|
||||
import cn.nianxx.thhotel.platform.security.service.CurrentUserContextService;
|
||||
import cn.nianxx.thhotel.platform.security.service.FrontendAuthorizationService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 前端业务接口强制鉴权服务实现。复用可选 token 解析结果,并返回业务接口专用错误码。
|
||||
*/
|
||||
@Service
|
||||
public class FrontendAuthorizationServiceImpl implements FrontendAuthorizationService {
|
||||
|
||||
private final CurrentUserContextService currentUserContextService;
|
||||
private final HttpServletRequest request;
|
||||
|
||||
/**
|
||||
* 注入当前用户上下文服务和原始请求,用于区分缺少 token 与 token 失效。
|
||||
*/
|
||||
public FrontendAuthorizationServiceImpl(
|
||||
CurrentUserContextService currentUserContextService,
|
||||
HttpServletRequest request) {
|
||||
this.currentUserContextService = currentUserContextService;
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
/**
|
||||
* 要求当前请求已登录;没有有效 token 时返回前端业务接口专用 401。
|
||||
*/
|
||||
@Override
|
||||
public AuthenticatedUserContext requireLogin() {
|
||||
return currentUserContextService.currentUser()
|
||||
.orElseThrow(this::missingOrInvalidLogin);
|
||||
}
|
||||
|
||||
/**
|
||||
* 要求当前请求用户拥有指定业务权限码;已登录但无权限时返回 403。
|
||||
*/
|
||||
@Override
|
||||
public AuthenticatedUserContext requirePermission(String permissionCode) {
|
||||
AuthenticatedUserContext context = requireLogin();
|
||||
if (permissionCode == null || permissionCode.isBlank()
|
||||
|| !context.permissionCodes().contains(permissionCode)) {
|
||||
throw new FrontendAuthorizationException(
|
||||
HttpStatus.FORBIDDEN,
|
||||
"FRONTEND_PERMISSION_DENIED",
|
||||
"当前用户没有访问该业务能力的权限。");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 Authorization 头判断是未登录还是登录态失效,便于前端做统一提示。
|
||||
*/
|
||||
private FrontendAuthorizationException missingOrInvalidLogin() {
|
||||
String authorizationHeader = request.getHeader("Authorization");
|
||||
if (hasBearerToken(authorizationHeader)) {
|
||||
return new FrontendAuthorizationException(
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
"AUTH_SESSION_INVALID",
|
||||
"登录已失效,请重新登录。");
|
||||
}
|
||||
return new FrontendAuthorizationException(
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
"AUTH_TOKEN_REQUIRED",
|
||||
"请先登录后再访问该业务能力。");
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断请求是否提供了非空 Bearer token。
|
||||
*/
|
||||
private boolean hasBearerToken(String authorizationHeader) {
|
||||
if (authorizationHeader == null || authorizationHeader.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
String prefix = "Bearer ";
|
||||
return authorizationHeader.regionMatches(true, 0, prefix, 0, prefix.length())
|
||||
&& !authorizationHeader.substring(prefix.length()).trim().isBlank();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.control;
|
||||
|
||||
import cn.nianxx.thhotel.platform.access.common.enums.PlatformPermissionCode;
|
||||
import cn.nianxx.thhotel.platform.security.service.FrontendAuthorizationService;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationOrderListQueryRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationTaskWorkbenchQueryRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationOrderDetailResult;
|
||||
@@ -21,12 +23,16 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
public class ReservationFrontendQueryController {
|
||||
|
||||
private final ReservationFrontendQueryService frontendQueryService;
|
||||
private final FrontendAuthorizationService authorizationService;
|
||||
|
||||
/**
|
||||
* 注入前端查询服务,Controller 只负责 HTTP 参数到查询对象的转换。
|
||||
*/
|
||||
public ReservationFrontendQueryController(ReservationFrontendQueryService frontendQueryService) {
|
||||
public ReservationFrontendQueryController(
|
||||
ReservationFrontendQueryService frontendQueryService,
|
||||
FrontendAuthorizationService authorizationService) {
|
||||
this.frontendQueryService = frontendQueryService;
|
||||
this.authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,6 +50,7 @@ public class ReservationFrontendQueryController {
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(name = "page_num", required = false) Integer pageNum,
|
||||
@RequestParam(name = "page_size", required = false) Integer pageSize) {
|
||||
authorizationService.requirePermission(PlatformPermissionCode.RESERVATION_TASK_READ.name());
|
||||
return frontendQueryService.queryTaskWorkbench(new ReservationTaskWorkbenchQueryRequest(
|
||||
hotelId,
|
||||
orderId,
|
||||
@@ -69,6 +76,7 @@ public class ReservationFrontendQueryController {
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(name = "page_num", required = false) Integer pageNum,
|
||||
@RequestParam(name = "page_size", required = false) Integer pageSize) {
|
||||
authorizationService.requirePermission(PlatformPermissionCode.RESERVATION_ORDER_READ.name());
|
||||
return frontendQueryService.queryOrders(new ReservationOrderListQueryRequest(
|
||||
hotelId,
|
||||
orderStatus,
|
||||
@@ -88,6 +96,7 @@ public class ReservationFrontendQueryController {
|
||||
@RequestParam(name = "hotel_id", required = false) String hotelId,
|
||||
@RequestParam(name = "include_tasks", required = false) Boolean includeTasks,
|
||||
@RequestParam(name = "include_source_summary", required = false) Boolean includeSourceSummary) {
|
||||
authorizationService.requirePermission(PlatformPermissionCode.RESERVATION_ORDER_READ.name());
|
||||
return frontendQueryService.getOrderDetail(hotelId, orderId, includeTasks, includeSourceSummary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.control;
|
||||
|
||||
import cn.nianxx.thhotel.platform.access.common.enums.PlatformPermissionCode;
|
||||
import cn.nianxx.thhotel.platform.security.service.FrontendAuthorizationService;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ManualReviewConversionRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ManualReviewResolutionRequest;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.request.ReservationOperaSimulationRequest;
|
||||
@@ -28,12 +30,16 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
public class ReservationTaskController {
|
||||
|
||||
private final ReservationTaskWorkflowService taskWorkflowService;
|
||||
private final FrontendAuthorizationService authorizationService;
|
||||
|
||||
/**
|
||||
* 注入任务工作流服务,Controller 不直接访问 Mapper 或 Repository。
|
||||
*/
|
||||
public ReservationTaskController(ReservationTaskWorkflowService taskWorkflowService) {
|
||||
public ReservationTaskController(
|
||||
ReservationTaskWorkflowService taskWorkflowService,
|
||||
FrontendAuthorizationService authorizationService) {
|
||||
this.taskWorkflowService = taskWorkflowService;
|
||||
this.authorizationService = authorizationService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,6 +47,7 @@ public class ReservationTaskController {
|
||||
*/
|
||||
@GetMapping(value = "/{taskId}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ReservationTaskDetailResult detail(@PathVariable Long taskId) {
|
||||
authorizationService.requirePermission(PlatformPermissionCode.RESERVATION_TASK_READ.name());
|
||||
return taskWorkflowService.getTaskDetail(taskId);
|
||||
}
|
||||
|
||||
@@ -129,6 +136,7 @@ public class ReservationTaskController {
|
||||
*/
|
||||
@GetMapping(value = "/{taskId}/audits", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ReservationTaskAuditListResult listAudits(@PathVariable Long taskId) {
|
||||
authorizationService.requirePermission(PlatformPermissionCode.RESERVATION_AUDIT_READ.name());
|
||||
return taskWorkflowService.listTaskAudits(taskId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.control;
|
||||
|
||||
import cn.nianxx.thhotel.platform.security.service.impl.FrontendAuthorizationException;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.result.ReservationWorkflowErrorResponse;
|
||||
import cn.nianxx.thhotel.workflows.reservation.service.impl.ReservationTaskWorkflowException;
|
||||
import java.util.List;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
@@ -28,4 +30,17 @@ public class ReservationTaskControllerAdvice {
|
||||
exception.getMessage(),
|
||||
exception.getDetails()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理前端业务接口登录或权限不足异常,保持 Reservation 错误响应结构稳定。
|
||||
*/
|
||||
@ExceptionHandler(FrontendAuthorizationException.class)
|
||||
public ResponseEntity<ReservationWorkflowErrorResponse> handleFrontendAuthorizationException(
|
||||
FrontendAuthorizationException exception) {
|
||||
return ResponseEntity.status(exception.getStatus())
|
||||
.body(new ReservationWorkflowErrorResponse(
|
||||
exception.getErrorCode(),
|
||||
exception.getMessage(),
|
||||
List.of()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -400,6 +400,17 @@ public class MybatisReservationAiWorkflowRepository implements ReservationAiWork
|
||||
return Optional.ofNullable(entity).map(this::toAiQueryOrderSnapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按订单 ID 查询前端详情快照。该方法不带酒店条件,仅供读取后再做酒店访问权校验。
|
||||
*/
|
||||
@Override
|
||||
public Optional<ReservationAiQueryOrderSnapshot> findAiQueryOrderById(Long orderId) {
|
||||
if (orderId == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.ofNullable(orderMapper.selectById(orderId)).map(this::toAiQueryOrderSnapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按订单 ID 批量查询订单快照,供前端任务列表补充订单展示键。
|
||||
*/
|
||||
|
||||
@@ -116,6 +116,11 @@ public interface ReservationAiWorkflowRepository {
|
||||
*/
|
||||
Optional<ReservationAiQueryOrderSnapshot> findAiQueryOrderById(String hotelId, Long orderId);
|
||||
|
||||
/**
|
||||
* 按订单 ID 查询前端详情使用的订单快照,调用方必须在读取后按订单实际酒店做访问权校验。
|
||||
*/
|
||||
Optional<ReservationAiQueryOrderSnapshot> findAiQueryOrderById(Long orderId);
|
||||
|
||||
/**
|
||||
* 按订单 ID 批量查询订单快照,供前端列表补充展示字段。
|
||||
*/
|
||||
|
||||
@@ -144,9 +144,8 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
Long orderId,
|
||||
Boolean includeTasks,
|
||||
Boolean includeSourceSummary) {
|
||||
String normalizedHotelId = normalizeHotelId(hotelId);
|
||||
ReservationAiQueryOrderSnapshot order = workflowRepository
|
||||
.findAiQueryOrderById(normalizedHotelId, orderId)
|
||||
.findAiQueryOrderById(orderId)
|
||||
.orElseThrow(() -> new ReservationTaskWorkflowException(
|
||||
HttpStatus.NOT_FOUND,
|
||||
"ORDER_NOT_FOUND",
|
||||
@@ -157,14 +156,15 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
"ORDER_NOT_FOUND",
|
||||
"订单不存在。");
|
||||
}
|
||||
String orderHotelId = requireOrderHotelAccess(order, hotelId);
|
||||
List<ReservationAiQueryTaskSnapshot> taskSnapshots = Boolean.FALSE.equals(includeTasks)
|
||||
? List.of()
|
||||
: workflowRepository.findAiQueryTasksByOrderIds(normalizedHotelId, List.of(order.id()));
|
||||
: workflowRepository.findAiQueryTasksByOrderIds(orderHotelId, List.of(order.id()));
|
||||
Map<Long, ReservationTaskAvailabilityResult> availabilityByTaskId = calculateAvailabilityByTaskId(
|
||||
taskSnapshots,
|
||||
taskSnapshots);
|
||||
Map<Long, SourceMessageDisplayContext> sourceContextsById = findSourceContextsById(
|
||||
normalizedHotelId,
|
||||
orderHotelId,
|
||||
taskSnapshots);
|
||||
List<ReservationOrderTaskTimelineItemResult> tasks = taskSnapshots.stream()
|
||||
.map(task -> toTimelineItem(
|
||||
@@ -567,6 +567,29 @@ public class ReservationFrontendQueryServiceImpl implements ReservationFrontendQ
|
||||
return order != null && ReservationOrderVisibility.HIDDEN_SYSTEM.name().equals(order.orderVisibility());
|
||||
}
|
||||
|
||||
/**
|
||||
* 按订单实际归属酒店校验当前用户访问权。详情接口不能只按请求 hotel_id 过滤,否则跨酒店 ID 会被误判为不存在。
|
||||
*/
|
||||
private String requireOrderHotelAccess(ReservationAiQueryOrderSnapshot order, String requestedHotelId) {
|
||||
try {
|
||||
String normalizedRequestedHotelId = trimToNull(requestedHotelId);
|
||||
String hotelIdToCheck = normalizedRequestedHotelId == null ? order.hotelId() : normalizedRequestedHotelId;
|
||||
String resolvedHotelId = hotelContextService.requireAccessibleHotel(hotelIdToCheck);
|
||||
if (!Objects.equals(order.hotelId(), resolvedHotelId)) {
|
||||
throw new ReservationTaskWorkflowException(
|
||||
HttpStatus.FORBIDDEN,
|
||||
"HOTEL_ACCESS_DENIED",
|
||||
"当前用户无权访问该酒店数据。");
|
||||
}
|
||||
return order.hotelId();
|
||||
} catch (HotelContextException exception) {
|
||||
throw new ReservationTaskWorkflowException(
|
||||
exception.getStatus(),
|
||||
exception.getErrorCode(),
|
||||
exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化酒店 ID。前端可不传酒店,后端按当前用户上下文或单酒店系统上下文解析。
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.service.impl;
|
||||
|
||||
import cn.nianxx.thhotel.platform.common.time.UtcTimeFormatter;
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextException;
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextService;
|
||||
import cn.nianxx.thhotel.platform.message.common.dto.SourceMessageSummaryResponse;
|
||||
import cn.nianxx.thhotel.platform.message.service.SourceMessageQueryService;
|
||||
import cn.nianxx.thhotel.workflows.reservation.common.dto.ReservationAuditLogDraft;
|
||||
@@ -95,6 +97,7 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
|
||||
private final ReservationTaskCardFieldDefinitionProvider fieldDefinitionProvider;
|
||||
private final ReservationTaskAvailabilityResolver availabilityResolver;
|
||||
private final SourceMessageQueryService sourceMessageQueryService;
|
||||
private final HotelContextService hotelContextService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
@@ -105,11 +108,13 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
|
||||
ReservationTaskCardFieldDefinitionProvider fieldDefinitionProvider,
|
||||
ReservationTaskAvailabilityResolver availabilityResolver,
|
||||
SourceMessageQueryService sourceMessageQueryService,
|
||||
HotelContextService hotelContextService,
|
||||
ObjectMapper objectMapper) {
|
||||
this.workflowRepository = workflowRepository;
|
||||
this.fieldDefinitionProvider = fieldDefinitionProvider;
|
||||
this.availabilityResolver = availabilityResolver;
|
||||
this.sourceMessageQueryService = sourceMessageQueryService;
|
||||
this.hotelContextService = hotelContextService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@@ -120,6 +125,7 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
|
||||
@Transactional(readOnly = true)
|
||||
public ReservationTaskDetailResult getTaskDetail(Long taskId) {
|
||||
ReservationTaskSnapshot task = findTaskOrThrow(taskId);
|
||||
requireTaskHotelAccess(task);
|
||||
ReservationTaskCardSnapshot taskCard = workflowRepository
|
||||
.findTaskCardByTaskId(task.hotelId(), task.id())
|
||||
.orElseThrow(() -> error(HttpStatus.NOT_FOUND, "TASK_CARD_NOT_FOUND", "任务卡不存在。"));
|
||||
@@ -403,6 +409,7 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
|
||||
@Transactional(readOnly = true)
|
||||
public ReservationTaskAuditListResult listTaskAudits(Long taskId) {
|
||||
ReservationTaskSnapshot task = findTaskOrThrow(taskId);
|
||||
requireTaskHotelAccess(task);
|
||||
List<ReservationTaskAuditLogResult> items = workflowRepository
|
||||
.findAuditLogsByTaskId(task.hotelId(), task.id())
|
||||
.stream()
|
||||
@@ -753,6 +760,20 @@ public class ReservationTaskWorkflowServiceImpl implements ReservationTaskWorkfl
|
||||
.orElseThrow(() -> error(HttpStatus.NOT_FOUND, "TASK_NOT_FOUND", "任务不存在。"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按任务实际归属酒店校验当前用户访问权,避免跨酒店按任务 ID 读取详情或审计。
|
||||
*/
|
||||
private void requireTaskHotelAccess(ReservationTaskSnapshot task) {
|
||||
try {
|
||||
hotelContextService.requireAccessibleHotel(task.hotelId());
|
||||
} catch (HotelContextException exception) {
|
||||
throw new ReservationTaskWorkflowException(
|
||||
exception.getStatus(),
|
||||
exception.getErrorCode(),
|
||||
exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询任务卡,不存在时抛出受控 404。
|
||||
*/
|
||||
|
||||
@@ -149,19 +149,21 @@ class AuthControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldKeepExistingBusinessEndpointsCompatibleWhenTokenIsMissingOrInvalid() throws Exception {
|
||||
void shouldRequireTokenForFrontendReadonlyBusinessEndpoints() throws Exception {
|
||||
mockMvc.perform(get("/api/reservation/orders")
|
||||
.param("hotel_id", "HOTEL-TEST")
|
||||
.param("page_num", "1")
|
||||
.param("page_size", "1"))
|
||||
.andExpect(status().isOk());
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andExpect(jsonPath("$.error_code").value("AUTH_TOKEN_REQUIRED"));
|
||||
|
||||
mockMvc.perform(get("/api/reservation/orders")
|
||||
.header("Authorization", "Bearer invalid-token")
|
||||
.param("hotel_id", "HOTEL-TEST")
|
||||
.param("page_num", "1")
|
||||
.param("page_size", "1"))
|
||||
.andExpect(status().isOk());
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andExpect(jsonPath("$.error_code").value("AUTH_SESSION_INVALID"));
|
||||
}
|
||||
|
||||
private MvcResult login() throws Exception {
|
||||
|
||||
@@ -3,6 +3,8 @@ package cn.nianxx.thhotel.platform.message.control;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static cn.nianxx.thhotel.support.MockMvcAuthTestSupport.loginToken;
|
||||
import static cn.nianxx.thhotel.support.MockMvcAuthTestSupport.performAuthorized;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
@@ -25,7 +27,17 @@ import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
@SpringBootTest(
|
||||
classes = ThHotelApplication.class,
|
||||
properties = "source-message.original-read.access-key=test-original-read-key")
|
||||
properties = {
|
||||
"source-message.original-read.access-key=test-original-read-key",
|
||||
"spring.datasource.url=jdbc:h2:mem:source_message_controller_test;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE",
|
||||
"auth.bootstrap.admin.username=source-message-admin",
|
||||
"auth.bootstrap.admin.password=Admin@123456",
|
||||
"auth.bootstrap.admin.display-name=SourceMessage管理员",
|
||||
"auth.bootstrap.default-hotel-id=HOTEL-TEST",
|
||||
"auth.bootstrap.default-hotel-name=测试酒店",
|
||||
"auth.bootstrap.default-hotel-time-zone=Asia/Bangkok",
|
||||
"auth.session.ttl-minutes=720"
|
||||
})
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
class SourceMessageControllerTest {
|
||||
@@ -39,6 +51,18 @@ class SourceMessageControllerTest {
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
private String adminToken;
|
||||
|
||||
/**
|
||||
* 获取 SourceMessage 摘要接口测试管理员 token。
|
||||
*/
|
||||
private String adminToken() throws Exception {
|
||||
if (adminToken == null) {
|
||||
adminToken = loginToken(mockMvc, "source-message-admin", "Admin@123456");
|
||||
}
|
||||
return adminToken;
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldListAndReadSummaryWithoutOriginalContentOrMediaUrls() throws Exception {
|
||||
SourceMessageCaptureResult result = captureService.capture(command(
|
||||
@@ -49,7 +73,7 @@ class SourceMessageControllerTest {
|
||||
"https://media.example.test/private.pdf?token=secret"
|
||||
));
|
||||
|
||||
mockMvc.perform(get("/api/source-messages")
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/source-messages")
|
||||
.param("hotelId", "HOTEL-TEST")
|
||||
.param("externalConversationId", "conversation-api-001")
|
||||
.param("pageNum", "1")
|
||||
@@ -66,7 +90,7 @@ class SourceMessageControllerTest {
|
||||
.andExpect(content().string(not(containsString("token=secret"))))
|
||||
.andExpect(content().string(not(containsString("payloadJson"))));
|
||||
|
||||
mockMvc.perform(get("/api/source-messages/{id}", result.inboxId()))
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/source-messages/{id}", result.inboxId()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.id").value(result.inboxId().toString()))
|
||||
.andExpect(jsonPath("$.externalMessageId").value("mail-api-001"))
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package cn.nianxx.thhotel.support;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.ResultActions;
|
||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
|
||||
|
||||
/**
|
||||
* MockMvc 登录辅助工具。测试仍走真实登录接口,避免绕过后端安全链路。
|
||||
*/
|
||||
public final class MockMvcAuthTestSupport {
|
||||
|
||||
private MockMvcAuthTestSupport() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过真实登录接口获取 Bearer token。
|
||||
*/
|
||||
public static String loginToken(MockMvc mockMvc, String username, String password) throws Exception {
|
||||
MvcResult result = mockMvc.perform(post("/api/auth/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""
|
||||
{
|
||||
"username": "%s",
|
||||
"password": "%s",
|
||||
"preferred_hotel_id": "HOTEL-TEST"
|
||||
}
|
||||
""".formatted(username, password)))
|
||||
.andExpect(status().isOk())
|
||||
.andReturn();
|
||||
return JsonPath.read(result.getResponse().getContentAsString(), "$.access_token");
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用指定 token 执行已授权请求。
|
||||
*/
|
||||
public static ResultActions performAuthorized(
|
||||
MockMvc mockMvc,
|
||||
String token,
|
||||
MockHttpServletRequestBuilder requestBuilder) throws Exception {
|
||||
return mockMvc.perform(requestBuilder.header("Authorization", "Bearer " + token));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package cn.nianxx.thhotel.workflows.reservation.control;
|
||||
|
||||
import static cn.nianxx.thhotel.support.MockMvcAuthTestSupport.loginToken;
|
||||
import static cn.nianxx.thhotel.support.MockMvcAuthTestSupport.performAuthorized;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
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 cn.nianxx.thhotel.ThHotelApplication;
|
||||
import cn.nianxx.thhotel.platform.hotel.repository.PlatformHotelRepository;
|
||||
import cn.nianxx.thhotel.platform.identity.common.enums.PlatformUserStatus;
|
||||
import cn.nianxx.thhotel.platform.identity.domain.PlatformUserEntity;
|
||||
import cn.nianxx.thhotel.platform.identity.repository.PlatformIdentityRepository;
|
||||
import cn.nianxx.thhotel.platform.identity.service.impl.AuthPasswordService;
|
||||
import cn.nianxx.thhotel.platform.message.common.request.CaptureSourceMessageCommand;
|
||||
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageCaptureResult;
|
||||
import cn.nianxx.thhotel.platform.message.service.SourceMessageCaptureService;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
@SpringBootTest(
|
||||
classes = ThHotelApplication.class,
|
||||
properties = {
|
||||
"spring.datasource.url=jdbc:h2:mem:frontend_read_auth_cp1;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE",
|
||||
"auth.bootstrap.admin.username=cp1-admin",
|
||||
"auth.bootstrap.admin.password=Admin@123456",
|
||||
"auth.bootstrap.admin.display-name=系统管理员",
|
||||
"auth.bootstrap.default-hotel-id=HOTEL-TEST",
|
||||
"auth.bootstrap.default-hotel-name=测试酒店",
|
||||
"auth.bootstrap.default-hotel-time-zone=Asia/Bangkok",
|
||||
"superagent.task-result.hmac-secret=test-superagent-secret"
|
||||
})
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
class FrontendReadAuthorizationControllerTest {
|
||||
|
||||
private static final String HOTEL_ID = "HOTEL-TEST";
|
||||
private static final String OTHER_HOTEL_ID = "HOTEL-OTHER";
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
@Autowired
|
||||
private SourceMessageCaptureService captureService;
|
||||
@Autowired
|
||||
private PlatformIdentityRepository identityRepository;
|
||||
@Autowired
|
||||
private PlatformHotelRepository hotelRepository;
|
||||
@Autowired
|
||||
private AuthPasswordService passwordService;
|
||||
|
||||
@BeforeEach
|
||||
void ensureNoPermissionUser() {
|
||||
PlatformUserEntity user = identityRepository.findUserByUsername("cp1-no-permission")
|
||||
.orElseGet(() -> {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
PlatformUserEntity created = new PlatformUserEntity();
|
||||
created.setUsername("cp1-no-permission");
|
||||
created.setPasswordHash(passwordService.hash("NoPerm@123456"));
|
||||
created.setDisplayName("无权限用户");
|
||||
created.setUserStatus(PlatformUserStatus.ACTIVE.name());
|
||||
created.setSuperAdmin(false);
|
||||
created.setPasswordChangedAt(now);
|
||||
created.setCreatedAt(now);
|
||||
created.setUpdatedAt(now);
|
||||
identityRepository.insertUser(created);
|
||||
return created;
|
||||
});
|
||||
hotelRepository.ensureUserHotel(user.getId(), HOTEL_ID, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectReservationReadWhenTokenMissing() throws Exception {
|
||||
mockMvc.perform(get("/api/reservation/tasks")
|
||||
.param("hotel_id", HOTEL_ID))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andExpect(jsonPath("$.error_code").value("AUTH_TOKEN_REQUIRED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectReservationReadWhenPermissionMissing() throws Exception {
|
||||
String token = loginToken(mockMvc, "cp1-no-permission", "NoPerm@123456");
|
||||
|
||||
performAuthorized(mockMvc, token, get("/api/reservation/tasks")
|
||||
.param("hotel_id", HOTEL_ID))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(jsonPath("$.error_code").value("FRONTEND_PERMISSION_DENIED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectReservationAndSourceMessageDetailAcrossHotels() throws Exception {
|
||||
String token = loginToken(mockMvc, "cp1-admin", "Admin@123456");
|
||||
SourceMessageCaptureResult source = captureOtherHotelSourceMessage();
|
||||
Long orderId = 970000000000000101L;
|
||||
Long taskId = 970000000000000301L;
|
||||
insertOtherHotelOrderTask(orderId, taskId, source.inboxId());
|
||||
|
||||
performAuthorized(mockMvc, token, get("/api/reservation/orders/{orderId}", orderId))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(jsonPath("$.error_code").value("HOTEL_ACCESS_DENIED"));
|
||||
|
||||
performAuthorized(mockMvc, token, get("/api/reservation/tasks/{taskId}", taskId))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(jsonPath("$.error_code").value("HOTEL_ACCESS_DENIED"));
|
||||
|
||||
performAuthorized(mockMvc, token, get("/api/reservation/tasks/{taskId}/audits", taskId))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(jsonPath("$.error_code").value("HOTEL_ACCESS_DENIED"));
|
||||
|
||||
performAuthorized(mockMvc, token, get("/api/source-messages/{id}", source.inboxId()))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(jsonPath("$.error_code").value("HOTEL_ACCESS_DENIED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldKeepSuperAgentTaskResultEndpointOutsideFrontendLoginInterceptor() throws Exception {
|
||||
mockMvc.perform(post("/api/integrations/superagent/task-results")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{}"))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andExpect(jsonPath("$.error_code").value("AUTH_HEADER_MISSING"));
|
||||
}
|
||||
|
||||
private SourceMessageCaptureResult captureOtherHotelSourceMessage() {
|
||||
return captureService.capture(new CaptureSourceMessageCommand(
|
||||
OTHER_HOTEL_ID,
|
||||
"AGENTBUS",
|
||||
"EMAIL",
|
||||
"cp1-other-hotel-mail-001",
|
||||
"cp1-other-hotel-thread-001",
|
||||
"frame-cp1-other",
|
||||
"session-cp1-other",
|
||||
Instant.parse("2026-07-13T01:00:00Z"),
|
||||
"guest@example.test",
|
||||
"Other hotel message",
|
||||
"Other hotel body",
|
||||
"<html><body>Other hotel body</body></html>",
|
||||
"{\"source\":{\"external_message_id\":\"cp1-other-hotel-mail-001\"}}",
|
||||
"agentbus-outlook-v1",
|
||||
List.of()));
|
||||
}
|
||||
|
||||
private void insertOtherHotelOrderTask(Long orderId, Long taskId, Long sourceMessageId) {
|
||||
Long transitionId = taskId - 1;
|
||||
jdbcTemplate.update("""
|
||||
INSERT INTO workflow_reservation_order (
|
||||
id, hotel_id, order_key_type, order_business_key, active_business_key,
|
||||
temporary_order_code, order_status, order_visibility, business_key_source, display_name,
|
||||
source_message_id, version, created_at, updated_at, latest_activity_at
|
||||
)
|
||||
VALUES (?, ?, 'GROUP_CODE', 'GRP-CP1-OTHER', 'GRP-CP1-OTHER',
|
||||
'TMP-CP1-OTHER', 'ACTIVE', 'VISIBLE', 'AI_CANDIDATE', 'GRP-CP1-OTHER',
|
||||
?, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
""", orderId, OTHER_HOTEL_ID, sourceMessageId);
|
||||
jdbcTemplate.update("""
|
||||
INSERT INTO workflow_reservation_ai_transition (
|
||||
id, hotel_id, batch_id, source_message_id, source_event_index, array_index,
|
||||
execution_order, catalog_code, skill_id, result_type, ai_task_type,
|
||||
system_task_type, task_card_type, task_subtype, current_or_history,
|
||||
group_code, item_payload_sha256, item_idempotency_key, blocked_until_parent_completed,
|
||||
ai_payload_json, case_keys_json, extracted_fields_json, created_at, updated_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, 1, 1, 1, 'S02', 'cp1_skill',
|
||||
'normal_task', 'New Booking', 'NEW_BOOKING', 'NEW_BOOKING', 'new_group_block',
|
||||
'current', 'GRP-CP1-OTHER', ?, ?, 0, '{}', '{}', '{}', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
""", transitionId, OTHER_HOTEL_ID, transitionId - 1, sourceMessageId, "1".repeat(64),
|
||||
transitionId.toString());
|
||||
jdbcTemplate.update("""
|
||||
INSERT INTO workflow_reservation_task (
|
||||
id, hotel_id, order_id, source_message_id, ai_transition_id,
|
||||
result_type, ai_task_type, system_task_type, task_card_type, task_subtype,
|
||||
task_status, queue_participation, execution_order, blocked_until_parent_completed,
|
||||
version, created_at, updated_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, 'normal_task', 'New Booking',
|
||||
'NEW_BOOKING', 'NEW_BOOKING', 'new_group_block', 'PENDING_CONFIRM',
|
||||
1, 1, 0, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
""", taskId, OTHER_HOTEL_ID, orderId, sourceMessageId, transitionId);
|
||||
jdbcTemplate.update("""
|
||||
INSERT INTO workflow_reservation_task_card (
|
||||
id, hotel_id, task_id, task_card_type, field_contract_version,
|
||||
ai_payload_json, created_at, updated_at
|
||||
)
|
||||
VALUES (?, ?, ?, 'NEW_BOOKING', 'test', '{}', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
""", taskId + 1, OTHER_HOTEL_ID, taskId);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import static org.hamcrest.Matchers.greaterThanOrEqualTo;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.Matchers.blankOrNullString;
|
||||
import static cn.nianxx.thhotel.support.MockMvcAuthTestSupport.loginToken;
|
||||
import static cn.nianxx.thhotel.support.MockMvcAuthTestSupport.performAuthorized;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
@@ -24,8 +26,16 @@ import org.springframework.test.web.servlet.MvcResult;
|
||||
@SpringBootTest(
|
||||
classes = ThHotelApplication.class,
|
||||
properties = {
|
||||
"spring.datasource.url=jdbc:h2:mem:reservation_demo_data_controller_test;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE",
|
||||
"reservation.demo-data.enabled=true",
|
||||
"reservation.demo-data.access-key=test-demo-data-key"
|
||||
"reservation.demo-data.access-key=test-demo-data-key",
|
||||
"auth.bootstrap.admin.username=demo-data-admin",
|
||||
"auth.bootstrap.admin.password=Admin@123456",
|
||||
"auth.bootstrap.admin.display-name=演示数据管理员",
|
||||
"auth.bootstrap.default-hotel-id=HOTEL-TEST",
|
||||
"auth.bootstrap.default-hotel-name=测试酒店",
|
||||
"auth.bootstrap.default-hotel-time-zone=Asia/Bangkok",
|
||||
"auth.session.ttl-minutes=720"
|
||||
})
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
@@ -37,6 +47,18 @@ class ReservationDemoDataControllerTest {
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
private String adminToken;
|
||||
|
||||
/**
|
||||
* 获取演示数据查询阶段使用的管理员 token。
|
||||
*/
|
||||
private String adminToken() throws Exception {
|
||||
if (adminToken == null) {
|
||||
adminToken = loginToken(mockMvc, "demo-data-admin", "Admin@123456");
|
||||
}
|
||||
return adminToken;
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectDemoDataSeedWhenAccessKeyMissing() throws Exception {
|
||||
mockMvc.perform(post("/api/system/reservation/demo-data")
|
||||
@@ -76,7 +98,7 @@ class ReservationDemoDataControllerTest {
|
||||
String blockedTaskId = first(response, "$.tasks[?(@.scenario_code=='QUEUE_BLOCKED')].task_id");
|
||||
String failedTaskId = first(response, "$.tasks[?(@.scenario_code=='OPERA_FAILED')].task_id");
|
||||
|
||||
mockMvc.perform(get("/api/reservation/tasks")
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks")
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("keyword", demoRunId)
|
||||
.param("page_num", "1")
|
||||
@@ -84,7 +106,7 @@ class ReservationDemoDataControllerTest {
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.items", hasSize(greaterThanOrEqualTo(5))));
|
||||
|
||||
mockMvc.perform(get("/api/reservation/orders")
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/orders")
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("keyword", demoRunId)
|
||||
.param("page_num", "1")
|
||||
@@ -92,7 +114,7 @@ class ReservationDemoDataControllerTest {
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.items", hasSize(greaterThanOrEqualTo(5))));
|
||||
|
||||
mockMvc.perform(get("/api/reservation/orders/{orderId}", queueOrderId)
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/orders/{orderId}", queueOrderId)
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("include_tasks", "true")
|
||||
.param("include_source_summary", "true"))
|
||||
@@ -100,13 +122,13 @@ class ReservationDemoDataControllerTest {
|
||||
.andExpect(jsonPath("$.tasks", hasSize(greaterThanOrEqualTo(2))))
|
||||
.andExpect(jsonPath("$.tasks[1].readonly_reason_code").value("PREVIOUS_TASK_NOT_FINISHED"));
|
||||
|
||||
mockMvc.perform(get("/api/reservation/tasks/{taskId}", blockedTaskId))
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks/{taskId}", blockedTaskId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.availability.blocked").value(true))
|
||||
.andExpect(jsonPath("$.availability.read_only").value(true))
|
||||
.andExpect(jsonPath("$.availability.blocked_by_task_id", not(blankOrNullString())));
|
||||
|
||||
mockMvc.perform(get("/api/reservation/tasks/{taskId}", failedTaskId))
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks/{taskId}", failedTaskId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.opera_operations[0].operation_status").value("FAILED"))
|
||||
.andExpect(jsonPath("$.opera_operations[0].attempt_count").value(1));
|
||||
|
||||
@@ -10,6 +10,8 @@ import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static cn.nianxx.thhotel.support.MockMvcAuthTestSupport.loginToken;
|
||||
import static cn.nianxx.thhotel.support.MockMvcAuthTestSupport.performAuthorized;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
@@ -40,7 +42,18 @@ import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
@SpringBootTest(classes = ThHotelApplication.class)
|
||||
@SpringBootTest(
|
||||
classes = ThHotelApplication.class,
|
||||
properties = {
|
||||
"spring.datasource.url=jdbc:h2:mem:reservation_frontend_query_test;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE",
|
||||
"auth.bootstrap.admin.username=frontend-query-admin",
|
||||
"auth.bootstrap.admin.password=Admin@123456",
|
||||
"auth.bootstrap.admin.display-name=前端查询管理员",
|
||||
"auth.bootstrap.default-hotel-id=HOTEL-TEST",
|
||||
"auth.bootstrap.default-hotel-name=测试酒店",
|
||||
"auth.bootstrap.default-hotel-time-zone=Asia/Bangkok",
|
||||
"auth.session.ttl-minutes=720"
|
||||
})
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
class ReservationFrontendQueryControllerTest {
|
||||
@@ -63,6 +76,18 @@ class ReservationFrontendQueryControllerTest {
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
private String adminToken;
|
||||
|
||||
/**
|
||||
* 获取前端查询管理员 token,测试通过真实登录接口覆盖认证链路。
|
||||
*/
|
||||
private String adminToken() throws Exception {
|
||||
if (adminToken == null) {
|
||||
adminToken = loginToken(mockMvc, "frontend-query-admin", "Admin@123456");
|
||||
}
|
||||
return adminToken;
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnTaskWorkbenchListWithRealtimeAvailability() throws Exception {
|
||||
SourceMessageCaptureResult source = captureSourceMessage(
|
||||
@@ -83,7 +108,7 @@ class ReservationFrontendQueryControllerTest {
|
||||
reset(sourceMessageQueryService);
|
||||
reset(workflowRepository);
|
||||
|
||||
mockMvc.perform(get("/api/reservation/tasks")
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks")
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("order_id", orderId.toString())
|
||||
.param("page_num", "1")
|
||||
@@ -119,7 +144,7 @@ class ReservationFrontendQueryControllerTest {
|
||||
.andExpect(jsonPath("$.page.page_size").value(20))
|
||||
.andExpect(jsonPath("$.page.total").value(2));
|
||||
|
||||
mockMvc.perform(get("/api/reservation/tasks")
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks")
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("keyword", "Frontend Query List Smoke")
|
||||
.param("page_num", "1")
|
||||
@@ -152,7 +177,7 @@ class ReservationFrontendQueryControllerTest {
|
||||
insertTask(endedTaskId, endedOrderId, source.inboxId(), 930000000000001202L, "Cancel Booking",
|
||||
"CANCEL_BOOKING", "CANCEL_BOOKING", "PENDING_CONFIRM", 1);
|
||||
|
||||
mockMvc.perform(get("/api/reservation/tasks")
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks")
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("order_status", "ENDED")
|
||||
.param("keyword", "GRP-FRONTEND-TASK-")
|
||||
@@ -191,7 +216,7 @@ class ReservationFrontendQueryControllerTest {
|
||||
insertTask(latestTaskId, latestOrderId, latestSource.inboxId(), 930000000000001502L, "Update Booking",
|
||||
"UPDATE_BOOKING", "UPDATE_BOOKING", "PENDING_CONFIRM", 1);
|
||||
|
||||
mockMvc.perform(get("/api/reservation/tasks")
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks")
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("keyword", "GRP-FRONTEND-TASK-SORT-")
|
||||
.param("page_num", "1")
|
||||
@@ -235,7 +260,7 @@ class ReservationFrontendQueryControllerTest {
|
||||
updateOrderLatestActivityAt(staleButRecentlyUpdatedOrderId, Instant.parse("2026-07-09T00:00:00Z"));
|
||||
updateOrderLatestActivityAt(latestActivityOrderId, Instant.parse("2026-07-10T09:30:00Z"));
|
||||
|
||||
mockMvc.perform(get("/api/reservation/orders")
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/orders")
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("keyword", "GRP-FRONTEND-ORDER-SORT-")
|
||||
.param("page_num", "1")
|
||||
@@ -276,7 +301,7 @@ class ReservationFrontendQueryControllerTest {
|
||||
"GRP-FRONTEND-VISIBLE-FILTER-ARCHIVED", "ACTIVE");
|
||||
updateOrderVisibility(archivedOrderId, "ARCHIVED_SYSTEM");
|
||||
|
||||
mockMvc.perform(get("/api/reservation/orders")
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/orders")
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("keyword", "GRP-FRONTEND-VISIBLE-FILTER-")
|
||||
.param("page_num", "1")
|
||||
@@ -355,7 +380,7 @@ class ReservationFrontendQueryControllerTest {
|
||||
"UPDATE_BOOKING", "UPDATE_BOOKING", "PENDING_CONFIRM", 2);
|
||||
reset(workflowRepository);
|
||||
|
||||
mockMvc.perform(get("/api/reservation/orders/{orderId}", orderId)
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/orders/{orderId}", orderId)
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("include_tasks", "true")
|
||||
.param("include_source_summary", "true"))
|
||||
@@ -387,7 +412,7 @@ class ReservationFrontendQueryControllerTest {
|
||||
.andExpect(jsonPath("$.warnings.length()").value(0));
|
||||
verify(workflowRepository, never()).findQueueTasksBefore(anyString(), anyLong(), any());
|
||||
|
||||
mockMvc.perform(get("/api/reservation/orders/{orderId}", orderId)
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/orders/{orderId}", orderId)
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("include_tasks", "false"))
|
||||
.andExpect(status().isOk())
|
||||
@@ -420,7 +445,7 @@ class ReservationFrontendQueryControllerTest {
|
||||
insertTask(failedTaskId, activeOrderId, source.inboxId(), 930000000000000803L, "Cancel Booking",
|
||||
"CANCEL_BOOKING", "CANCEL_BOOKING", "FAILED", 3);
|
||||
|
||||
mockMvc.perform(get("/api/reservation/orders")
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/orders")
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("keyword", "GRP-FRONTEND-ORDERS-")
|
||||
.param("page_num", "1")
|
||||
@@ -439,7 +464,7 @@ class ReservationFrontendQueryControllerTest {
|
||||
.value(contains(0)))
|
||||
.andExpect(jsonPath("$.page.total").value(2));
|
||||
|
||||
mockMvc.perform(get("/api/reservation/orders")
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/orders")
|
||||
.param("hotel_id", HOTEL_ID)
|
||||
.param("order_status", "ENDED")
|
||||
.param("keyword", "GRP-FRONTEND-ORDERS-")
|
||||
|
||||
@@ -6,6 +6,8 @@ import static org.hamcrest.Matchers.matchesPattern;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static cn.nianxx.thhotel.support.MockMvcAuthTestSupport.loginToken;
|
||||
import static cn.nianxx.thhotel.support.MockMvcAuthTestSupport.performAuthorized;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
@@ -40,11 +42,19 @@ import org.springframework.test.web.servlet.MvcResult;
|
||||
@SpringBootTest(
|
||||
classes = ThHotelApplication.class,
|
||||
properties = {
|
||||
"spring.datasource.url=jdbc:h2:mem:superagent_task_result_controller_test;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE",
|
||||
"superagent.task-result.hmac-secret=test-superagent-secret",
|
||||
"superagent.task-result.clock-skew-seconds=300",
|
||||
"superagent.task-result.nonce-ttl-seconds=600",
|
||||
"superagent.task-result.max-body-bytes=12000",
|
||||
"superagent.task-result.allow-legacy-internal-source-message-id=true"
|
||||
"superagent.task-result.allow-legacy-internal-source-message-id=true",
|
||||
"auth.bootstrap.admin.username=superagent-result-admin",
|
||||
"auth.bootstrap.admin.password=Admin@123456",
|
||||
"auth.bootstrap.admin.display-name=SuperAgent回归管理员",
|
||||
"auth.bootstrap.default-hotel-id=HOTEL-TEST",
|
||||
"auth.bootstrap.default-hotel-name=测试酒店",
|
||||
"auth.bootstrap.default-hotel-time-zone=Asia/Bangkok",
|
||||
"auth.session.ttl-minutes=720"
|
||||
})
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
@@ -64,6 +74,18 @@ class SuperAgentTaskResultControllerTest {
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
private String adminToken;
|
||||
|
||||
/**
|
||||
* 获取 SuperAgent 回调测试里前端只读接口验证使用的管理员 token。
|
||||
*/
|
||||
private String adminToken() throws Exception {
|
||||
if (adminToken == null) {
|
||||
adminToken = loginToken(mockMvc, "superagent-result-admin", "Admin@123456");
|
||||
}
|
||||
return adminToken;
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectRequestWithoutHmacHeaders() throws Exception {
|
||||
String body = minimalBody("1", "New Booking", "normal_task", "new_fit_reservation", """
|
||||
@@ -522,7 +544,7 @@ class SuperAgentTaskResultControllerTest {
|
||||
String taskId = com.jayway.jsonpath.JsonPath.read(result.getResponse().getContentAsString(), "$.items[0].task_id");
|
||||
String orderId = com.jayway.jsonpath.JsonPath.read(result.getResponse().getContentAsString(), "$.items[0].order_id");
|
||||
|
||||
mockMvc.perform(get("/api/reservation/tasks/{taskId}", taskId))
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks/{taskId}", taskId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.system_task_type").value("SOURCE_MESSAGE_ONLY"))
|
||||
.andExpect(jsonPath("$.task_card_type").value("SOURCE_MESSAGE_ONLY"))
|
||||
@@ -560,7 +582,7 @@ class SuperAgentTaskResultControllerTest {
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.error_code").value("TASK_NOT_MANUAL_REVIEW"));
|
||||
|
||||
mockMvc.perform(get("/api/reservation/tasks")
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks")
|
||||
.param("hotel_id", "HOTEL-TEST")
|
||||
.param("task_type", "SOURCE_MESSAGE_ONLY")
|
||||
.param("keyword", "mail-s000-entry-result-001"))
|
||||
@@ -572,7 +594,7 @@ class SuperAgentTaskResultControllerTest {
|
||||
.andExpect(jsonPath("$.items[0].queue_participation").value(false))
|
||||
.andExpect(jsonPath("$.items[0].can_process").value(false));
|
||||
|
||||
mockMvc.perform(get("/api/reservation/orders")
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/orders")
|
||||
.param("hotel_id", "HOTEL-TEST")
|
||||
.param("keyword", "mail-s000-entry-result-001"))
|
||||
.andExpect(status().isOk())
|
||||
@@ -871,7 +893,7 @@ class SuperAgentTaskResultControllerTest {
|
||||
"$.items[0].task_id"
|
||||
);
|
||||
|
||||
mockMvc.perform(get("/api/reservation/tasks")
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks")
|
||||
.param("hotel_id", "HOTEL-TEST")
|
||||
.param("task_type", "SOURCE_MESSAGE_ONLY")
|
||||
.param("keyword", "mail-v3-s99-frontend-visible-001"))
|
||||
@@ -882,7 +904,7 @@ class SuperAgentTaskResultControllerTest {
|
||||
.andExpect(jsonPath("$.items[0].route_code").value("S99"))
|
||||
.andExpect(jsonPath("$.items[0].system_process_category").value("SOURCE_MESSAGE_NOTIFICATION"));
|
||||
|
||||
mockMvc.perform(get("/api/reservation/tasks/{taskId}", taskId))
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks/{taskId}", taskId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.result_type").value("source_message_review_notification"))
|
||||
.andExpect(jsonPath("$.ai_task_type").value("S99"))
|
||||
@@ -1465,7 +1487,7 @@ class SuperAgentTaskResultControllerTest {
|
||||
"$.items[1].task_id"
|
||||
);
|
||||
|
||||
mockMvc.perform(get("/api/reservation/tasks")
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks")
|
||||
.param("hotel_id", "HOTEL-TEST")
|
||||
.param("keyword", "mail-v3-frontend-route-blocks-001"))
|
||||
.andExpect(status().isOk())
|
||||
@@ -1475,7 +1497,7 @@ class SuperAgentTaskResultControllerTest {
|
||||
.andExpect(jsonPath("$.items[0].route_code").value("R01_NEW_FIT_RESERVATION_NORMAL"))
|
||||
.andExpect(jsonPath("$.items[0].system_process_category").value("BUSINESS_TASK"));
|
||||
|
||||
mockMvc.perform(get("/api/reservation/orders/{orderId}", orderId)
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/orders/{orderId}", orderId)
|
||||
.param("hotel_id", "HOTEL-TEST")
|
||||
.param("include_tasks", "true"))
|
||||
.andExpect(status().isOk())
|
||||
@@ -1485,7 +1507,7 @@ class SuperAgentTaskResultControllerTest {
|
||||
.andExpect(jsonPath("$.tasks[0].route_code").value("R01_NEW_FIT_RESERVATION_NORMAL"))
|
||||
.andExpect(jsonPath("$.tasks[0].system_process_category").value("BUSINESS_TASK"));
|
||||
|
||||
mockMvc.perform(get("/api/reservation/tasks/{taskId}", taskId))
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks/{taskId}", taskId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.result_type").value("normal_task"))
|
||||
.andExpect(jsonPath("$.ai_task_type").value("New Booking"))
|
||||
@@ -1646,7 +1668,7 @@ class SuperAgentTaskResultControllerTest {
|
||||
String firstTaskId = com.jayway.jsonpath.JsonPath.read(result.getResponse().getContentAsString(), "$.items[0].task_id");
|
||||
String secondTaskId = com.jayway.jsonpath.JsonPath.read(result.getResponse().getContentAsString(), "$.items[1].task_id");
|
||||
|
||||
mockMvc.perform(get("/api/reservation/tasks/{taskId}", secondTaskId))
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks/{taskId}", secondTaskId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.task_id").value(secondTaskId))
|
||||
.andExpect(jsonPath("$.availability.blocked").value(true))
|
||||
@@ -1662,7 +1684,7 @@ class SuperAgentTaskResultControllerTest {
|
||||
WHERE id = ?
|
||||
""", Long.valueOf(firstTaskId));
|
||||
|
||||
mockMvc.perform(get("/api/reservation/tasks/{taskId}", secondTaskId))
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks/{taskId}", secondTaskId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.availability.blocked").value(true))
|
||||
.andExpect(jsonPath("$.availability.blocked_by_task_id").value(firstTaskId));
|
||||
@@ -1673,7 +1695,7 @@ class SuperAgentTaskResultControllerTest {
|
||||
WHERE id = ?
|
||||
""", Long.valueOf(firstTaskId));
|
||||
|
||||
mockMvc.perform(get("/api/reservation/tasks/{taskId}", secondTaskId))
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks/{taskId}", secondTaskId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.availability.blocked").value(true))
|
||||
.andExpect(jsonPath("$.availability.blocked_by_task_id").value(firstTaskId));
|
||||
@@ -1684,7 +1706,7 @@ class SuperAgentTaskResultControllerTest {
|
||||
WHERE id = ?
|
||||
""", Long.valueOf(firstTaskId));
|
||||
|
||||
mockMvc.perform(get("/api/reservation/tasks/{taskId}", secondTaskId))
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks/{taskId}", secondTaskId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.availability.blocked").value(false))
|
||||
.andExpect(jsonPath("$.availability.read_only").value(false))
|
||||
@@ -1705,7 +1727,7 @@ class SuperAgentTaskResultControllerTest {
|
||||
.andReturn();
|
||||
String taskId = com.jayway.jsonpath.JsonPath.read(result.getResponse().getContentAsString(), "$.items[0].task_id");
|
||||
|
||||
mockMvc.perform(get("/api/reservation/tasks/{taskId}", taskId))
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks/{taskId}", taskId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.task_card_type").value("NEW_BOOKING"))
|
||||
.andExpect(jsonPath("$.source_subject").value("M002 SuperAgent intake"))
|
||||
@@ -1820,7 +1842,7 @@ class SuperAgentTaskResultControllerTest {
|
||||
.andExpect(jsonPath("$.opera_operations[1].operation_sequence").value(2))
|
||||
.andExpect(jsonPath("$.opera_operations[1].operation_status").value("PENDING"));
|
||||
|
||||
mockMvc.perform(get("/api/reservation/tasks/{taskId}", taskId))
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks/{taskId}", taskId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.opera_operations.length()").value(2))
|
||||
.andExpect(jsonPath("$.opera_operations[0].operation_sequence").value(1))
|
||||
@@ -1938,7 +1960,7 @@ class SuperAgentTaskResultControllerTest {
|
||||
.andExpect(jsonPath("$.attempts[0].attempt_number").value(1))
|
||||
.andExpect(jsonPath("$.attempts[0].attempt_status").value("SUCCEEDED"));
|
||||
|
||||
mockMvc.perform(get("/api/reservation/tasks/{taskId}", taskId))
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks/{taskId}", taskId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.opera_operations[0].operation_id").value(firstOperationId))
|
||||
.andExpect(jsonPath("$.opera_operations[0].operation_status").value("SUCCEEDED"))
|
||||
@@ -2065,7 +2087,7 @@ class SuperAgentTaskResultControllerTest {
|
||||
"""))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
mockMvc.perform(get("/api/reservation/tasks/{taskId}/audits", taskId))
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks/{taskId}/audits", taskId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.task_id").value(taskId))
|
||||
.andExpect(jsonPath("$.items[?(@.action=='TASK_CONFIRM')].action")
|
||||
@@ -2208,7 +2230,7 @@ class SuperAgentTaskResultControllerTest {
|
||||
WHERE source_message_id = ?
|
||||
""", Long.class, source.inboxId());
|
||||
|
||||
mockMvc.perform(get("/api/reservation/tasks/{taskId}", taskId))
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks/{taskId}", taskId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.result_type").value("manual_review"))
|
||||
.andExpect(jsonPath("$.review_status").value("PENDING"))
|
||||
|
||||
@@ -3,6 +3,8 @@ package cn.nianxx.thhotel.workflows.reservation.control;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static cn.nianxx.thhotel.support.MockMvcAuthTestSupport.loginToken;
|
||||
import static cn.nianxx.thhotel.support.MockMvcAuthTestSupport.performAuthorized;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
|
||||
@@ -40,10 +42,18 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilde
|
||||
@SpringBootTest(
|
||||
classes = ThHotelApplication.class,
|
||||
properties = {
|
||||
"spring.datasource.url=jdbc:h2:mem:superagent_task_result_p0_fixture_test;MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE",
|
||||
"superagent.task-result.hmac-secret=test-superagent-secret",
|
||||
"superagent.task-result.clock-skew-seconds=300",
|
||||
"superagent.task-result.nonce-ttl-seconds=600",
|
||||
"superagent.task-result.max-body-bytes=20000"
|
||||
"superagent.task-result.max-body-bytes=20000",
|
||||
"auth.bootstrap.admin.username=p0-fixture-admin",
|
||||
"auth.bootstrap.admin.password=Admin@123456",
|
||||
"auth.bootstrap.admin.display-name=P0基线管理员",
|
||||
"auth.bootstrap.default-hotel-id=HOTEL-TEST",
|
||||
"auth.bootstrap.default-hotel-name=测试酒店",
|
||||
"auth.bootstrap.default-hotel-time-zone=Asia/Bangkok",
|
||||
"auth.session.ttl-minutes=720"
|
||||
})
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
@@ -73,6 +83,18 @@ class SuperAgentTaskResultP0FixtureRegressionTest {
|
||||
@Autowired
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
private String adminToken;
|
||||
|
||||
/**
|
||||
* 获取 P0 fixture 回归测试里前端只读接口验证使用的管理员 token。
|
||||
*/
|
||||
private String adminToken() throws Exception {
|
||||
if (adminToken == null) {
|
||||
adminToken = loginToken(mockMvc, "p0-fixture-admin", "Admin@123456");
|
||||
}
|
||||
return adminToken;
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAcceptLegalS10AndS99MainOutcomeFixtures() throws Exception {
|
||||
JsonNode cases = fixture("main_outcomes.json").path("cases");
|
||||
@@ -99,7 +121,7 @@ class SuperAgentTaskResultP0FixtureRegressionTest {
|
||||
String s10TaskId = com.jayway.jsonpath.JsonPath.read(
|
||||
s10Result.getResponse().getContentAsString(),
|
||||
"$.items[0].task_id");
|
||||
mockMvc.perform(get("/api/reservation/tasks/{taskId}", s10TaskId))
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks/{taskId}", s10TaskId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.fields.length()").value(0));
|
||||
|
||||
@@ -466,7 +488,7 @@ class SuperAgentTaskResultP0FixtureRegressionTest {
|
||||
createResult.getResponse().getContentAsString(),
|
||||
"$.items[2].order_id");
|
||||
|
||||
mockMvc.perform(get("/api/reservation/tasks/{taskId}", taskId))
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks/{taskId}", taskId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.fields[?(@.field_path=='case_keys.group_code')].field_pointer")
|
||||
.value(contains("/case_keys/group_code")));
|
||||
@@ -679,7 +701,7 @@ class SuperAgentTaskResultP0FixtureRegressionTest {
|
||||
result.getResponse().getContentAsString(),
|
||||
"$.items[0].task_id");
|
||||
|
||||
mockMvc.perform(get("/api/reservation/tasks/{taskId}", taskId))
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks/{taskId}", taskId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.fields[?(@.field_path=='extracted_fields.room_items.0.room_quantity')].field_pointer")
|
||||
.value(contains("/extracted_fields/room_items/0/room_quantity")))
|
||||
@@ -856,7 +878,7 @@ class SuperAgentTaskResultP0FixtureRegressionTest {
|
||||
result.getResponse().getContentAsString(),
|
||||
"$.items[0].order_id");
|
||||
|
||||
mockMvc.perform(get("/api/reservation/tasks/{taskId}", taskId))
|
||||
performAuthorized(mockMvc, adminToken(), get("/api/reservation/tasks/{taskId}", taskId))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.fields[?(@.field_path=='extracted_fields.room_items.0.pms_room_type_code')].field_pointer")
|
||||
.value(contains("/extracted_fields/room_items/0/pms_room_type_code")))
|
||||
|
||||
Reference in New Issue
Block a user