mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-08 03:07:51 +00:00
fix(admin): replace compat and admin placeholders with real queries
Implement compat search through SkillSearchAppService instead of returning an empty placeholder list, and map search results back to canonical slugs for the compatibility API. Replace hard-coded admin user and audit-log payloads with repository-backed application services. User management now supports paged search and status filters, validates managed statuses and role codes, prevents USER_ADMIN from assigning SUPER_ADMIN, and persists role/status changes against the real repositories. Audit logs now read from the audit_log table through a dedicated query repository/service with filterable pagination. Align admin response DTOs with the frontend contract, add domain not-found handling for localized 404 responses, and cover the new behavior with controller and service regression tests. Verified with targeted skillhub-app tests plus full server mvn test.
This commit is contained in:
parent
ad8bb9c6fd
commit
383bc1edae
18 changed files with 700 additions and 47 deletions
|
|
@ -1,28 +1,56 @@
|
|||
package com.iflytek.skillhub.compat;
|
||||
|
||||
import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
|
||||
import com.iflytek.skillhub.compat.dto.ClawHubSkillItem;
|
||||
import com.iflytek.skillhub.compat.dto.ClawHubResolveResponse;
|
||||
import com.iflytek.skillhub.compat.dto.ClawHubSearchResponse;
|
||||
import com.iflytek.skillhub.compat.dto.ClawHubWhoamiResponse;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
import com.iflytek.skillhub.service.SkillSearchAppService;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/compat/v1")
|
||||
public class ClawHubCompatController {
|
||||
|
||||
private final CanonicalSlugMapper mapper;
|
||||
private final SkillSearchAppService skillSearchAppService;
|
||||
|
||||
public ClawHubCompatController(CanonicalSlugMapper mapper) {
|
||||
public ClawHubCompatController(CanonicalSlugMapper mapper, SkillSearchAppService skillSearchAppService) {
|
||||
this.mapper = mapper;
|
||||
this.skillSearchAppService = skillSearchAppService;
|
||||
}
|
||||
|
||||
@GetMapping("/search")
|
||||
public ClawHubSearchResponse search(@RequestParam String q) {
|
||||
// Return empty results for now (placeholder)
|
||||
return new ClawHubSearchResponse(List.of());
|
||||
public ClawHubSearchResponse search(
|
||||
@RequestParam String q,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int limit,
|
||||
@RequestAttribute(value = "userId", required = false) String userId,
|
||||
@RequestAttribute(value = "userNsRoles", required = false) Map<Long, NamespaceRole> userNsRoles) {
|
||||
SkillSearchAppService.SearchResponse response = skillSearchAppService.search(
|
||||
q,
|
||||
null,
|
||||
q == null || q.isBlank() ? "newest" : "relevance",
|
||||
page,
|
||||
limit,
|
||||
userId,
|
||||
userNsRoles
|
||||
);
|
||||
|
||||
List<ClawHubSkillItem> items = response.items().stream()
|
||||
.map(item -> new ClawHubSkillItem(
|
||||
mapper.toCanonical(item.namespace(), item.slug()),
|
||||
item.summary(),
|
||||
item.latestVersion(),
|
||||
item.starCount()))
|
||||
.toList();
|
||||
|
||||
return new ClawHubSearchResponse(items);
|
||||
}
|
||||
|
||||
@GetMapping("/resolve/{canonicalSlug}")
|
||||
|
|
|
|||
|
|
@ -5,19 +5,20 @@ import com.iflytek.skillhub.dto.ApiResponse;
|
|||
import com.iflytek.skillhub.dto.ApiResponseFactory;
|
||||
import com.iflytek.skillhub.dto.AuditLogItemResponse;
|
||||
import com.iflytek.skillhub.dto.PageResponse;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import com.iflytek.skillhub.service.AdminAuditLogAppService;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/admin/audit-logs")
|
||||
public class AuditLogController extends BaseApiController {
|
||||
|
||||
public AuditLogController(ApiResponseFactory responseFactory) {
|
||||
private final AdminAuditLogAppService adminAuditLogAppService;
|
||||
|
||||
public AuditLogController(AdminAuditLogAppService adminAuditLogAppService,
|
||||
ApiResponseFactory responseFactory) {
|
||||
super(responseFactory);
|
||||
this.adminAuditLogAppService = adminAuditLogAppService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
|
|
@ -27,15 +28,6 @@ public class AuditLogController extends BaseApiController {
|
|||
@RequestParam(defaultValue = "20") int size,
|
||||
@RequestParam(required = false) String userId,
|
||||
@RequestParam(required = false) String action) {
|
||||
List<AuditLogItemResponse> logs = List.of(
|
||||
new AuditLogItemResponse(
|
||||
"log-1", "user-1", "CREATE_SKILL", "SKILL", "skill-123", Instant.now(), "192.168.1.1"
|
||||
),
|
||||
new AuditLogItemResponse(
|
||||
"log-2", "user-2", "UPDATE_NAMESPACE", "NAMESPACE", "ns-456",
|
||||
Instant.now().minusSeconds(3600), "192.168.1.2"
|
||||
)
|
||||
);
|
||||
return ok("response.success.read", PageResponse.from(new PageImpl<>(logs)));
|
||||
return ok("response.success.read", adminAuditLogAppService.listAuditLogs(page, size, userId, action));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.iflytek.skillhub.controller.admin;
|
||||
|
||||
import com.iflytek.skillhub.controller.BaseApiController;
|
||||
import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
|
||||
import com.iflytek.skillhub.dto.AdminUserMutationResponse;
|
||||
import com.iflytek.skillhub.dto.AdminUserRoleUpdateRequest;
|
||||
import com.iflytek.skillhub.dto.AdminUserStatusUpdateRequest;
|
||||
|
|
@ -8,39 +9,42 @@ import com.iflytek.skillhub.dto.AdminUserSummaryResponse;
|
|||
import com.iflytek.skillhub.dto.ApiResponse;
|
||||
import com.iflytek.skillhub.dto.ApiResponseFactory;
|
||||
import com.iflytek.skillhub.dto.PageResponse;
|
||||
import com.iflytek.skillhub.service.AdminUserAppService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/admin/users")
|
||||
public class UserManagementController extends BaseApiController {
|
||||
|
||||
public UserManagementController(ApiResponseFactory responseFactory) {
|
||||
private final AdminUserAppService adminUserAppService;
|
||||
|
||||
public UserManagementController(AdminUserAppService adminUserAppService,
|
||||
ApiResponseFactory responseFactory) {
|
||||
super(responseFactory);
|
||||
this.adminUserAppService = adminUserAppService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAnyRole('USER_ADMIN', 'SUPER_ADMIN')")
|
||||
public ApiResponse<PageResponse<AdminUserSummaryResponse>> listUsers(
|
||||
@RequestParam(required = false) String search,
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size) {
|
||||
List<AdminUserSummaryResponse> users = List.of(
|
||||
new AdminUserSummaryResponse("user-1", "alice", "USER", "ACTIVE"),
|
||||
new AdminUserSummaryResponse("user-2", "bob", "USER", "ACTIVE")
|
||||
);
|
||||
return ok("response.success.read", PageResponse.from(new PageImpl<>(users)));
|
||||
return ok("response.success.read", adminUserAppService.listUsers(search, status, page, size));
|
||||
}
|
||||
|
||||
@PutMapping("/{userId}/role")
|
||||
@PreAuthorize("hasAnyRole('USER_ADMIN', 'SUPER_ADMIN')")
|
||||
public ApiResponse<AdminUserMutationResponse> updateUserRole(
|
||||
@PathVariable String userId,
|
||||
@AuthenticationPrincipal PlatformPrincipal principal,
|
||||
@Valid @RequestBody AdminUserRoleUpdateRequest request) {
|
||||
return ok("response.success.updated", new AdminUserMutationResponse(userId, request.role(), null));
|
||||
return ok("response.success.updated",
|
||||
adminUserAppService.updateUserRole(userId, request.role(), principal.platformRoles()));
|
||||
}
|
||||
|
||||
@PutMapping("/{userId}/status")
|
||||
|
|
@ -48,6 +52,6 @@ public class UserManagementController extends BaseApiController {
|
|||
public ApiResponse<AdminUserMutationResponse> updateUserStatus(
|
||||
@PathVariable String userId,
|
||||
@Valid @RequestBody AdminUserStatusUpdateRequest request) {
|
||||
return ok("response.success.updated", new AdminUserMutationResponse(userId, null, request.status()));
|
||||
return ok("response.success.updated", adminUserAppService.updateUserStatus(userId, request.status()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
package com.iflytek.skillhub.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public record AdminUserSummaryResponse(
|
||||
String userId,
|
||||
String id,
|
||||
String username,
|
||||
String role,
|
||||
String status
|
||||
String email,
|
||||
String status,
|
||||
List<String> platformRoles,
|
||||
LocalDateTime createdAt
|
||||
) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ package com.iflytek.skillhub.dto;
|
|||
import java.time.Instant;
|
||||
|
||||
public record AuditLogItemResponse(
|
||||
String id,
|
||||
String userId,
|
||||
Long id,
|
||||
String action,
|
||||
String resourceType,
|
||||
String resourceId,
|
||||
Instant timestamp,
|
||||
String ipAddress
|
||||
String userId,
|
||||
String username,
|
||||
String details,
|
||||
String ipAddress,
|
||||
Instant timestamp
|
||||
) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.iflytek.skillhub.dto.ApiResponse;
|
|||
import com.iflytek.skillhub.dto.ApiResponseFactory;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainNotFoundException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.slf4j.MDC;
|
||||
|
|
@ -43,6 +44,12 @@ public class GlobalExceptionHandler {
|
|||
apiResponseFactory.error(403, ex.messageCode(), ex.messageArgs()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(DomainNotFoundException.class)
|
||||
public ResponseEntity<ApiResponse<Void>> handleDomainNotFound(DomainNotFoundException ex) {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(
|
||||
apiResponseFactory.error(404, ex.messageCode(), ex.messageArgs()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<ApiResponse<Void>> handleValidation(MethodArgumentNotValidException ex) {
|
||||
String msg = ex.getBindingResult().getFieldErrors().stream()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
package com.iflytek.skillhub.repository;
|
||||
|
||||
import com.iflytek.skillhub.domain.user.UserAccount;
|
||||
import com.iflytek.skillhub.domain.user.UserStatus;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.TypedQuery;
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
@Repository
|
||||
public class AdminUserSearchRepository {
|
||||
|
||||
private final EntityManager entityManager;
|
||||
|
||||
public AdminUserSearchRepository(EntityManager entityManager) {
|
||||
this.entityManager = entityManager;
|
||||
}
|
||||
|
||||
public Page<UserAccount> search(String search, UserStatus status, Pageable pageable) {
|
||||
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
|
||||
|
||||
CriteriaQuery<UserAccount> query = builder.createQuery(UserAccount.class);
|
||||
Root<UserAccount> root = query.from(UserAccount.class);
|
||||
List<Predicate> predicates = buildPredicates(search, status, builder, root);
|
||||
query.select(root)
|
||||
.where(predicates.toArray(Predicate[]::new))
|
||||
.orderBy(builder.desc(root.get("createdAt")));
|
||||
|
||||
TypedQuery<UserAccount> typedQuery = entityManager.createQuery(query);
|
||||
typedQuery.setFirstResult((int) pageable.getOffset());
|
||||
typedQuery.setMaxResults(pageable.getPageSize());
|
||||
List<UserAccount> users = typedQuery.getResultList();
|
||||
|
||||
CriteriaQuery<Long> countQuery = builder.createQuery(Long.class);
|
||||
Root<UserAccount> countRoot = countQuery.from(UserAccount.class);
|
||||
List<Predicate> countPredicates = buildPredicates(search, status, builder, countRoot);
|
||||
countQuery.select(builder.count(countRoot))
|
||||
.where(countPredicates.toArray(Predicate[]::new));
|
||||
long total = entityManager.createQuery(countQuery).getSingleResult();
|
||||
|
||||
return new PageImpl<>(users, pageable, total);
|
||||
}
|
||||
|
||||
private List<Predicate> buildPredicates(
|
||||
String search,
|
||||
UserStatus status,
|
||||
CriteriaBuilder builder,
|
||||
Root<UserAccount> root) {
|
||||
List<Predicate> predicates = new ArrayList<>();
|
||||
if (StringUtils.hasText(search)) {
|
||||
String normalized = "%" + search.trim().toLowerCase(Locale.ROOT) + "%";
|
||||
predicates.add(builder.or(
|
||||
builder.like(builder.lower(root.get("id")), normalized),
|
||||
builder.like(builder.lower(root.get("displayName")), normalized),
|
||||
builder.like(builder.lower(root.get("email")), normalized)
|
||||
));
|
||||
}
|
||||
if (status != null) {
|
||||
predicates.add(builder.equal(root.get("status"), status));
|
||||
}
|
||||
return predicates;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package com.iflytek.skillhub.service;
|
||||
|
||||
import com.iflytek.skillhub.dto.AuditLogItemResponse;
|
||||
import com.iflytek.skillhub.dto.PageResponse;
|
||||
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class AdminAuditLogAppService {
|
||||
|
||||
private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;
|
||||
|
||||
public AdminAuditLogAppService(NamedParameterJdbcTemplate namedParameterJdbcTemplate) {
|
||||
this.namedParameterJdbcTemplate = namedParameterJdbcTemplate;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageResponse<AuditLogItemResponse> listAuditLogs(int page, int size, String userId, String action) {
|
||||
MapSqlParameterSource parameters = new MapSqlParameterSource()
|
||||
.addValue("limit", size)
|
||||
.addValue("offset", Math.max(page, 0) * size);
|
||||
|
||||
String whereClause = buildWhereClause(parameters, userId, action);
|
||||
Long total = namedParameterJdbcTemplate.queryForObject(
|
||||
"SELECT COUNT(*) FROM audit_log al" + whereClause,
|
||||
parameters,
|
||||
Long.class
|
||||
);
|
||||
|
||||
List<AuditLogItemResponse> items = namedParameterJdbcTemplate.query(
|
||||
"""
|
||||
SELECT al.id,
|
||||
al.action,
|
||||
al.actor_user_id,
|
||||
ua.display_name,
|
||||
al.detail_json,
|
||||
al.target_type,
|
||||
al.target_id,
|
||||
al.client_ip,
|
||||
al.created_at
|
||||
FROM audit_log al
|
||||
LEFT JOIN user_account ua ON ua.id = al.actor_user_id
|
||||
""" + whereClause + """
|
||||
ORDER BY al.created_at DESC
|
||||
LIMIT :limit OFFSET :offset
|
||||
""",
|
||||
parameters,
|
||||
(rs, rowNum) -> new AuditLogItemResponse(
|
||||
rs.getLong("id"),
|
||||
rs.getString("action"),
|
||||
rs.getString("actor_user_id"),
|
||||
rs.getString("display_name"),
|
||||
renderDetails(
|
||||
rs.getString("detail_json"),
|
||||
rs.getString("target_type"),
|
||||
rs.getObject("target_id")),
|
||||
rs.getString("client_ip"),
|
||||
toInstant(rs.getTimestamp("created_at")))
|
||||
);
|
||||
|
||||
return new PageResponse<>(items, total == null ? 0 : total, page, size);
|
||||
}
|
||||
|
||||
private String buildWhereClause(MapSqlParameterSource parameters, String userId, String action) {
|
||||
StringBuilder clause = new StringBuilder(" WHERE 1 = 1");
|
||||
if (StringUtils.hasText(userId)) {
|
||||
clause.append(" AND al.actor_user_id = :userId");
|
||||
parameters.addValue("userId", userId.trim());
|
||||
}
|
||||
if (StringUtils.hasText(action)) {
|
||||
clause.append(" AND al.action = :action");
|
||||
parameters.addValue("action", action.trim());
|
||||
}
|
||||
return clause.toString();
|
||||
}
|
||||
|
||||
private String renderDetails(String detailJson, String targetType, Object targetId) {
|
||||
if (StringUtils.hasText(detailJson)) {
|
||||
return detailJson;
|
||||
}
|
||||
if (!StringUtils.hasText(targetType) && targetId == null) {
|
||||
return null;
|
||||
}
|
||||
return targetType + ":" + targetId;
|
||||
}
|
||||
|
||||
private Instant toInstant(Timestamp timestamp) {
|
||||
return timestamp == null ? null : timestamp.toInstant();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
package com.iflytek.skillhub.service;
|
||||
|
||||
import com.iflytek.skillhub.auth.entity.Role;
|
||||
import com.iflytek.skillhub.auth.entity.UserRoleBinding;
|
||||
import com.iflytek.skillhub.auth.repository.RoleRepository;
|
||||
import com.iflytek.skillhub.auth.repository.UserRoleBindingRepository;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainNotFoundException;
|
||||
import com.iflytek.skillhub.domain.user.UserAccount;
|
||||
import com.iflytek.skillhub.domain.user.UserAccountRepository;
|
||||
import com.iflytek.skillhub.domain.user.UserStatus;
|
||||
import com.iflytek.skillhub.dto.AdminUserMutationResponse;
|
||||
import com.iflytek.skillhub.dto.AdminUserSummaryResponse;
|
||||
import com.iflytek.skillhub.dto.PageResponse;
|
||||
import com.iflytek.skillhub.repository.AdminUserSearchRepository;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class AdminUserAppService {
|
||||
|
||||
private static final Set<UserStatus> MANAGEABLE_STATUSES = Set.of(UserStatus.ACTIVE, UserStatus.DISABLED);
|
||||
|
||||
private final AdminUserSearchRepository adminUserSearchRepository;
|
||||
private final UserAccountRepository userAccountRepository;
|
||||
private final UserRoleBindingRepository userRoleBindingRepository;
|
||||
private final RoleRepository roleRepository;
|
||||
|
||||
public AdminUserAppService(
|
||||
AdminUserSearchRepository adminUserSearchRepository,
|
||||
UserAccountRepository userAccountRepository,
|
||||
UserRoleBindingRepository userRoleBindingRepository,
|
||||
RoleRepository roleRepository) {
|
||||
this.adminUserSearchRepository = adminUserSearchRepository;
|
||||
this.userAccountRepository = userAccountRepository;
|
||||
this.userRoleBindingRepository = userRoleBindingRepository;
|
||||
this.roleRepository = roleRepository;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageResponse<AdminUserSummaryResponse> listUsers(String search, String status, int page, int size) {
|
||||
Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt"));
|
||||
Page<UserAccount> result = adminUserSearchRepository.search(
|
||||
search,
|
||||
StringUtils.hasText(status) ? parseStatus(status) : null,
|
||||
pageable
|
||||
);
|
||||
Map<String, List<String>> rolesByUserId = loadRolesByUserId(
|
||||
result.getContent().stream().map(UserAccount::getId).toList());
|
||||
|
||||
List<AdminUserSummaryResponse> items = result.getContent().stream()
|
||||
.map(user -> new AdminUserSummaryResponse(
|
||||
user.getId(),
|
||||
user.getDisplayName(),
|
||||
user.getEmail(),
|
||||
user.getStatus().name(),
|
||||
rolesByUserId.getOrDefault(user.getId(), List.of()),
|
||||
user.getCreatedAt()))
|
||||
.toList();
|
||||
|
||||
return new PageResponse<>(items, result.getTotalElements(), result.getNumber(), result.getSize());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AdminUserMutationResponse updateUserRole(String userId, String roleCode, Set<String> actorPlatformRoles) {
|
||||
UserAccount user = loadUser(userId);
|
||||
String normalizedRoleCode = normalizeRoleCode(roleCode);
|
||||
|
||||
if ("SUPER_ADMIN".equals(normalizedRoleCode)
|
||||
&& (actorPlatformRoles == null || !actorPlatformRoles.contains("SUPER_ADMIN"))) {
|
||||
throw new DomainForbiddenException("error.admin.user.role.superAdmin.assignDenied");
|
||||
}
|
||||
|
||||
userRoleBindingRepository.deleteByUserId(user.getId());
|
||||
|
||||
if (!"USER".equals(normalizedRoleCode)) {
|
||||
Role role = roleRepository.findByCode(normalizedRoleCode)
|
||||
.orElseThrow(() -> new DomainBadRequestException("error.admin.user.role.invalid", roleCode));
|
||||
userRoleBindingRepository.save(new UserRoleBinding(user.getId(), role));
|
||||
}
|
||||
|
||||
return new AdminUserMutationResponse(user.getId(), normalizedRoleCode, user.getStatus().name());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AdminUserMutationResponse updateUserStatus(String userId, String status) {
|
||||
UserAccount user = loadUser(userId);
|
||||
UserStatus nextStatus = parseManageableStatus(status);
|
||||
user.setStatus(nextStatus);
|
||||
userAccountRepository.save(user);
|
||||
return new AdminUserMutationResponse(user.getId(), null, nextStatus.name());
|
||||
}
|
||||
|
||||
private UserStatus parseManageableStatus(String status) {
|
||||
UserStatus parsedStatus = parseStatus(status);
|
||||
if (!MANAGEABLE_STATUSES.contains(parsedStatus)) {
|
||||
throw new DomainBadRequestException("error.admin.user.status.unsupported");
|
||||
}
|
||||
return parsedStatus;
|
||||
}
|
||||
|
||||
private UserStatus parseStatus(String status) {
|
||||
try {
|
||||
return UserStatus.valueOf(status.trim().toUpperCase(Locale.ROOT));
|
||||
} catch (IllegalArgumentException ex) {
|
||||
throw new DomainBadRequestException("error.admin.user.status.invalid", status);
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeRoleCode(String roleCode) {
|
||||
if (!StringUtils.hasText(roleCode)) {
|
||||
throw new DomainBadRequestException("error.admin.user.role.invalid", roleCode);
|
||||
}
|
||||
return roleCode.trim().toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private Map<String, List<String>> loadRolesByUserId(List<String> userIds) {
|
||||
if (userIds.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
return userRoleBindingRepository.findByUserIdIn(userIds).stream()
|
||||
.collect(Collectors.groupingBy(
|
||||
UserRoleBinding::getUserId,
|
||||
Collectors.mapping(binding -> binding.getRole().getCode(),
|
||||
Collectors.collectingAndThen(Collectors.toList(),
|
||||
roles -> roles.stream().sorted().toList()))));
|
||||
}
|
||||
|
||||
private UserAccount loadUser(String userId) {
|
||||
return userAccountRepository.findById(userId)
|
||||
.orElseThrow(() -> new DomainNotFoundException("error.admin.user.notFound", userId));
|
||||
}
|
||||
}
|
||||
|
|
@ -71,3 +71,8 @@ error.deviceAuth.userCode.invalid=Invalid or expired user code
|
|||
error.deviceAuth.deviceCode.expired=Device code expired
|
||||
error.deviceAuth.deviceCode.invalid=Device code expired or invalid
|
||||
error.deviceAuth.deviceCode.used=Device code has already been used
|
||||
error.admin.user.notFound=User not found: {0}
|
||||
error.admin.user.role.invalid=Invalid role: {0}
|
||||
error.admin.user.role.superAdmin.assignDenied=Only SUPER_ADMIN can assign SUPER_ADMIN role
|
||||
error.admin.user.status.invalid=Invalid user status: {0}
|
||||
error.admin.user.status.unsupported=Only ACTIVE or DISABLED status can be managed here
|
||||
|
|
|
|||
|
|
@ -71,3 +71,8 @@ error.deviceAuth.userCode.invalid=无效或已过期的用户验证码
|
|||
error.deviceAuth.deviceCode.expired=设备验证码已过期
|
||||
error.deviceAuth.deviceCode.invalid=设备验证码无效或已过期
|
||||
error.deviceAuth.deviceCode.used=设备验证码已被使用
|
||||
error.admin.user.notFound=鐢ㄦ埛涓嶅瓨鍦細{0}
|
||||
error.admin.user.role.invalid=鏃犳晥鐨勮鑹诧細{0}
|
||||
error.admin.user.role.superAdmin.assignDenied=鍙湁 SUPER_ADMIN 鍙互鍒嗛厤 SUPER_ADMIN 瑙掕壊
|
||||
error.admin.user.status.invalid=鏃犳晥鐨勭敤鎴风姸鎬侊細{0}
|
||||
error.admin.user.status.unsupported=杩欓噷鍙厑璁告寜 ACTIVE 鎴?DISABLED 绠$悊鐢ㄦ埛鐘舵€?
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package com.iflytek.skillhub.compat;
|
|||
import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository;
|
||||
import com.iflytek.skillhub.auth.device.DeviceAuthService;
|
||||
import com.iflytek.skillhub.dto.SkillSummaryResponse;
|
||||
import com.iflytek.skillhub.service.SkillSearchAppService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
|
|
@ -15,7 +17,10 @@ import org.springframework.test.web.servlet.MockMvc;
|
|||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
|
|
@ -35,13 +40,38 @@ class ClawHubCompatControllerTest {
|
|||
@MockBean
|
||||
private DeviceAuthService deviceAuthService;
|
||||
|
||||
@MockBean
|
||||
private SkillSearchAppService skillSearchAppService;
|
||||
|
||||
@Test
|
||||
void search_returns_200() throws Exception {
|
||||
void search_returns_mapped_results() throws Exception {
|
||||
when(skillSearchAppService.search("test", null, "relevance", 0, 20, null, null))
|
||||
.thenReturn(new SkillSearchAppService.SearchResponse(
|
||||
List.of(new SkillSummaryResponse(
|
||||
1L,
|
||||
"my-skill",
|
||||
"My Skill",
|
||||
"test summary",
|
||||
10L,
|
||||
5,
|
||||
BigDecimal.valueOf(4.5),
|
||||
2,
|
||||
"1.2.0",
|
||||
"global",
|
||||
LocalDateTime.of(2026, 3, 13, 9, 0))),
|
||||
1,
|
||||
0,
|
||||
20
|
||||
));
|
||||
|
||||
mockMvc.perform(get("/api/compat/v1/search")
|
||||
.param("q", "test"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.items").isArray())
|
||||
.andExpect(jsonPath("$.items").isEmpty());
|
||||
.andExpect(jsonPath("$.items[0].canonicalSlug").value("my-skill"))
|
||||
.andExpect(jsonPath("$.items[0].description").value("test summary"))
|
||||
.andExpect(jsonPath("$.items[0].latestVersion").value("1.2.0"))
|
||||
.andExpect(jsonPath("$.items[0].starCount").value(5));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import com.iflytek.skillhub.TestRedisConfig;
|
|||
import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
|
||||
import com.iflytek.skillhub.auth.device.DeviceAuthService;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository;
|
||||
import com.iflytek.skillhub.dto.AuditLogItemResponse;
|
||||
import com.iflytek.skillhub.dto.PageResponse;
|
||||
import com.iflytek.skillhub.service.AdminAuditLogAppService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
|
|
@ -17,7 +20,9 @@ import org.springframework.test.web.servlet.MockMvc;
|
|||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.time.Instant;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
|
|
@ -38,6 +43,9 @@ class AuditLogControllerTest {
|
|||
@MockBean
|
||||
private DeviceAuthService deviceAuthService;
|
||||
|
||||
@MockBean
|
||||
private AdminAuditLogAppService adminAuditLogAppService;
|
||||
|
||||
@Test
|
||||
void listAuditLogs_unauthenticated_returns401() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/admin/audit-logs"))
|
||||
|
|
@ -53,11 +61,27 @@ class AuditLogControllerTest {
|
|||
principal, null, List.of(new SimpleGrantedAuthority("ROLE_AUDITOR"))
|
||||
);
|
||||
|
||||
when(adminAuditLogAppService.listAuditLogs(0, 20, null, null))
|
||||
.thenReturn(new PageResponse<>(
|
||||
List.of(new AuditLogItemResponse(
|
||||
1L,
|
||||
"USER_STATUS_CHANGE",
|
||||
"user-1",
|
||||
"alice",
|
||||
"{\"status\":\"DISABLED\"}",
|
||||
"127.0.0.1",
|
||||
Instant.parse("2026-03-13T01:00:00Z"))),
|
||||
1,
|
||||
0,
|
||||
20));
|
||||
|
||||
mockMvc.perform(get("/api/v1/admin/audit-logs").with(authentication(auth)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.items").isArray())
|
||||
.andExpect(jsonPath("$.data.total").value(2));
|
||||
.andExpect(jsonPath("$.data.total").value(1))
|
||||
.andExpect(jsonPath("$.data.items[0].username").value("alice"))
|
||||
.andExpect(jsonPath("$.data.items[0].details").value("{\"status\":\"DISABLED\"}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -69,6 +93,9 @@ class AuditLogControllerTest {
|
|||
principal, null, List.of(new SimpleGrantedAuthority("ROLE_SUPER_ADMIN"))
|
||||
);
|
||||
|
||||
when(adminAuditLogAppService.listAuditLogs(0, 20, null, null))
|
||||
.thenReturn(new PageResponse<>(List.of(), 0, 0, 20));
|
||||
|
||||
mockMvc.perform(get("/api/v1/admin/audit-logs").with(authentication(auth)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.items").isArray());
|
||||
|
|
@ -83,6 +110,9 @@ class AuditLogControllerTest {
|
|||
principal, null, List.of(new SimpleGrantedAuthority("ROLE_AUDITOR"))
|
||||
);
|
||||
|
||||
when(adminAuditLogAppService.listAuditLogs(0, 20, "user-1", "CREATE_SKILL"))
|
||||
.thenReturn(new PageResponse<>(List.of(), 0, 0, 20));
|
||||
|
||||
mockMvc.perform(get("/api/v1/admin/audit-logs")
|
||||
.param("userId", "user-1")
|
||||
.param("action", "CREATE_SKILL")
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ import com.iflytek.skillhub.TestRedisConfig;
|
|||
import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
|
||||
import com.iflytek.skillhub.auth.device.DeviceAuthService;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository;
|
||||
import com.iflytek.skillhub.dto.AdminUserMutationResponse;
|
||||
import com.iflytek.skillhub.dto.AdminUserSummaryResponse;
|
||||
import com.iflytek.skillhub.dto.PageResponse;
|
||||
import com.iflytek.skillhub.service.AdminUserAppService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
|
|
@ -17,6 +21,7 @@ import org.springframework.test.web.servlet.MockMvc;
|
|||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
||||
|
|
@ -25,6 +30,7 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
|
|||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
|
|
@ -41,6 +47,9 @@ class UserManagementControllerTest {
|
|||
@MockBean
|
||||
private DeviceAuthService deviceAuthService;
|
||||
|
||||
@MockBean
|
||||
private AdminUserAppService adminUserAppService;
|
||||
|
||||
@Test
|
||||
void listUsers_unauthenticated_returns401() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/admin/users"))
|
||||
|
|
@ -56,11 +65,27 @@ class UserManagementControllerTest {
|
|||
principal, null, List.of(new SimpleGrantedAuthority("ROLE_USER_ADMIN"))
|
||||
);
|
||||
|
||||
when(adminUserAppService.listUsers(null, null, 0, 20))
|
||||
.thenReturn(new PageResponse<>(
|
||||
List.of(new AdminUserSummaryResponse(
|
||||
"user-1",
|
||||
"alice",
|
||||
"alice@example.com",
|
||||
"ACTIVE",
|
||||
List.of("AUDITOR"),
|
||||
LocalDateTime.of(2026, 3, 13, 9, 0))),
|
||||
1,
|
||||
0,
|
||||
20));
|
||||
|
||||
mockMvc.perform(get("/api/v1/admin/users").with(authentication(auth)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.items").isArray())
|
||||
.andExpect(jsonPath("$.data.total").value(2));
|
||||
.andExpect(jsonPath("$.data.total").value(1))
|
||||
.andExpect(jsonPath("$.data.items[0].id").value("user-1"))
|
||||
.andExpect(jsonPath("$.data.items[0].email").value("alice@example.com"))
|
||||
.andExpect(jsonPath("$.data.items[0].platformRoles[0]").value("AUDITOR"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -72,6 +97,9 @@ class UserManagementControllerTest {
|
|||
principal, null, List.of(new SimpleGrantedAuthority("ROLE_SUPER_ADMIN"))
|
||||
);
|
||||
|
||||
when(adminUserAppService.listUsers(null, null, 0, 20))
|
||||
.thenReturn(new PageResponse<>(List.of(), 0, 0, 20));
|
||||
|
||||
mockMvc.perform(get("/api/v1/admin/users").with(authentication(auth)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.items").isArray());
|
||||
|
|
@ -88,6 +116,9 @@ class UserManagementControllerTest {
|
|||
|
||||
String requestBody = "{\"role\":\"MODERATOR\"}";
|
||||
|
||||
when(adminUserAppService.updateUserRole("user-123", "MODERATOR", Set.of("USER_ADMIN")))
|
||||
.thenReturn(new AdminUserMutationResponse("user-123", "MODERATOR", "ACTIVE"));
|
||||
|
||||
mockMvc.perform(put("/api/v1/admin/users/user-123/role")
|
||||
.with(authentication(auth))
|
||||
.with(csrf())
|
||||
|
|
@ -96,7 +127,8 @@ class UserManagementControllerTest {
|
|||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.userId").value("user-123"))
|
||||
.andExpect(jsonPath("$.data.role").value("MODERATOR"));
|
||||
.andExpect(jsonPath("$.data.role").value("MODERATOR"))
|
||||
.andExpect(jsonPath("$.data.status").value("ACTIVE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -108,7 +140,10 @@ class UserManagementControllerTest {
|
|||
principal, null, List.of(new SimpleGrantedAuthority("ROLE_USER_ADMIN"))
|
||||
);
|
||||
|
||||
String requestBody = "{\"status\":\"BANNED\"}";
|
||||
String requestBody = "{\"status\":\"DISABLED\"}";
|
||||
|
||||
when(adminUserAppService.updateUserStatus("user-123", "DISABLED"))
|
||||
.thenReturn(new AdminUserMutationResponse("user-123", null, "DISABLED"));
|
||||
|
||||
mockMvc.perform(put("/api/v1/admin/users/user-123/status")
|
||||
.with(authentication(auth))
|
||||
|
|
@ -118,6 +153,6 @@ class UserManagementControllerTest {
|
|||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.userId").value("user-123"))
|
||||
.andExpect(jsonPath("$.data.status").value("BANNED"));
|
||||
.andExpect(jsonPath("$.data.status").value("DISABLED"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
package com.iflytek.skillhub.service;
|
||||
|
||||
import com.iflytek.skillhub.dto.AuditLogItemResponse;
|
||||
import com.iflytek.skillhub.dto.PageResponse;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class AdminAuditLogAppServiceTest {
|
||||
|
||||
private final NamedParameterJdbcTemplate jdbcTemplate = mock(NamedParameterJdbcTemplate.class);
|
||||
private final AdminAuditLogAppService service = new AdminAuditLogAppService(jdbcTemplate);
|
||||
|
||||
@Test
|
||||
void listAuditLogs_returnsJdbcBackedPage() {
|
||||
when(jdbcTemplate.queryForObject(contains("COUNT(*)"), any(MapSqlParameterSource.class), eq(Long.class)))
|
||||
.thenReturn(1L);
|
||||
when(jdbcTemplate.query(contains("FROM audit_log"), any(MapSqlParameterSource.class), any(RowMapper.class)))
|
||||
.thenReturn(List.of(new AuditLogItemResponse(
|
||||
1L,
|
||||
"USER_STATUS_CHANGE",
|
||||
"user-1",
|
||||
"alice",
|
||||
"{\"status\":\"DISABLED\"}",
|
||||
"127.0.0.1",
|
||||
Instant.parse("2026-03-13T01:00:00Z")
|
||||
)));
|
||||
|
||||
PageResponse<?> response = service.listAuditLogs(0, 20, "user-1", "USER_STATUS_CHANGE");
|
||||
|
||||
assertThat(response.total()).isEqualTo(1);
|
||||
assertThat(response.items()).hasSize(1);
|
||||
verify(jdbcTemplate).queryForObject(contains("al.actor_user_id = :userId"), any(MapSqlParameterSource.class), eq(Long.class));
|
||||
verify(jdbcTemplate).query(contains("al.action = :action"), any(MapSqlParameterSource.class), any(RowMapper.class));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
package com.iflytek.skillhub.service;
|
||||
|
||||
import com.iflytek.skillhub.auth.entity.Role;
|
||||
import com.iflytek.skillhub.auth.entity.UserRoleBinding;
|
||||
import com.iflytek.skillhub.auth.repository.RoleRepository;
|
||||
import com.iflytek.skillhub.auth.repository.UserRoleBindingRepository;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainForbiddenException;
|
||||
import com.iflytek.skillhub.domain.shared.exception.DomainNotFoundException;
|
||||
import com.iflytek.skillhub.domain.user.UserAccount;
|
||||
import com.iflytek.skillhub.domain.user.UserAccountRepository;
|
||||
import com.iflytek.skillhub.domain.user.UserStatus;
|
||||
import com.iflytek.skillhub.dto.PageResponse;
|
||||
import com.iflytek.skillhub.repository.AdminUserSearchRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class AdminUserAppServiceTest {
|
||||
|
||||
private final AdminUserSearchRepository adminUserSearchRepository = mock(AdminUserSearchRepository.class);
|
||||
private final UserRoleBindingRepository userRoleBindingRepository = mock(UserRoleBindingRepository.class);
|
||||
private final RoleRepository roleRepository = mock(RoleRepository.class);
|
||||
private final UserAccountRepository userAccountRepository = mock(UserAccountRepository.class);
|
||||
private final AdminUserAppService service = new AdminUserAppService(
|
||||
adminUserSearchRepository,
|
||||
userAccountRepository,
|
||||
userRoleBindingRepository,
|
||||
roleRepository
|
||||
);
|
||||
|
||||
@Test
|
||||
void listUsers_returnsPagedUsersFromRepository() {
|
||||
UserAccount user = user("user-1", "alice", "alice@example.com", UserStatus.ACTIVE);
|
||||
PageRequest pageable = PageRequest.of(0, 20, Sort.by(Sort.Direction.DESC, "createdAt"));
|
||||
when(adminUserSearchRepository.search("ali", UserStatus.ACTIVE, pageable))
|
||||
.thenReturn(new PageImpl<>(List.of(user), pageable, 1));
|
||||
when(userRoleBindingRepository.findByUserIdIn(List.of("user-1")))
|
||||
.thenReturn(List.of(new UserRoleBinding("user-1", role("AUDITOR"))));
|
||||
|
||||
PageResponse<?> response = service.listUsers("ali", "ACTIVE", 0, 20);
|
||||
|
||||
assertThat(response.total()).isEqualTo(1);
|
||||
assertThat(response.items()).hasSize(1);
|
||||
assertThat(response.items().get(0)).extracting("id", "username", "email", "status")
|
||||
.containsExactly("user-1", "alice", "alice@example.com", "ACTIVE");
|
||||
assertThat(response.items().get(0)).extracting("platformRoles")
|
||||
.isEqualTo(List.of("AUDITOR"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void listUsers_withInvalidStatus_throwsBadRequest() {
|
||||
assertThrows(DomainBadRequestException.class, () -> service.listUsers(null, "BANNED", 0, 20));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateUserRole_nonSuperAdminCannotAssignSuperAdmin() {
|
||||
when(userAccountRepository.findById("user-1"))
|
||||
.thenReturn(Optional.of(user("user-1", "alice", "alice@example.com", UserStatus.ACTIVE)));
|
||||
|
||||
assertThrows(DomainForbiddenException.class,
|
||||
() -> service.updateUserRole("user-1", "SUPER_ADMIN", Set.of("USER_ADMIN")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateUserRole_replacesExistingBindings() {
|
||||
when(userAccountRepository.findById("user-1"))
|
||||
.thenReturn(Optional.of(user("user-1", "alice", "alice@example.com", UserStatus.ACTIVE)));
|
||||
when(roleRepository.findByCode("AUDITOR")).thenReturn(Optional.of(role("AUDITOR")));
|
||||
|
||||
var response = service.updateUserRole("user-1", "AUDITOR", Set.of("SUPER_ADMIN"));
|
||||
|
||||
verify(userRoleBindingRepository).deleteByUserId("user-1");
|
||||
verify(userRoleBindingRepository).save(any(UserRoleBinding.class));
|
||||
assertThat(response.userId()).isEqualTo("user-1");
|
||||
assertThat(response.role()).isEqualTo("AUDITOR");
|
||||
assertThat(response.status()).isEqualTo("ACTIVE");
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateUserRole_userPseudoRoleClearsBindingsWithoutSavingNewRole() {
|
||||
when(userAccountRepository.findById("user-1"))
|
||||
.thenReturn(Optional.of(user("user-1", "alice", "alice@example.com", UserStatus.ACTIVE)));
|
||||
|
||||
var response = service.updateUserRole("user-1", "USER", Set.of("SUPER_ADMIN"));
|
||||
|
||||
verify(userRoleBindingRepository).deleteByUserId("user-1");
|
||||
verify(userRoleBindingRepository, never()).save(any(UserRoleBinding.class));
|
||||
assertThat(response.role()).isEqualTo("USER");
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateUserStatus_rejectsUnsupportedStatuses() {
|
||||
when(userAccountRepository.findById("user-1"))
|
||||
.thenReturn(Optional.of(user("user-1", "alice", "alice@example.com", UserStatus.ACTIVE)));
|
||||
|
||||
assertThrows(DomainBadRequestException.class, () -> service.updateUserStatus("user-1", "MERGED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateUserStatus_updatesPersistedStatus() {
|
||||
UserAccount user = user("user-1", "alice", "alice@example.com", UserStatus.ACTIVE);
|
||||
when(userAccountRepository.findById("user-1")).thenReturn(Optional.of(user));
|
||||
when(userAccountRepository.save(user)).thenReturn(user);
|
||||
|
||||
var response = service.updateUserStatus("user-1", "DISABLED");
|
||||
|
||||
verify(userAccountRepository).save(user);
|
||||
assertThat(user.getStatus()).isEqualTo(UserStatus.DISABLED);
|
||||
assertThat(response.status()).isEqualTo("DISABLED");
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateUserStatus_withUnknownUser_throwsNotFound() {
|
||||
when(userAccountRepository.findById("missing")).thenReturn(Optional.empty());
|
||||
|
||||
assertThrows(DomainNotFoundException.class, () -> service.updateUserStatus("missing", "DISABLED"));
|
||||
}
|
||||
|
||||
private UserAccount user(String id, String displayName, String email, UserStatus status) {
|
||||
UserAccount user = new UserAccount(id, displayName, email, null);
|
||||
user.setStatus(status);
|
||||
ReflectionTestUtils.setField(user, "createdAt", LocalDateTime.of(2026, 3, 13, 9, 0));
|
||||
ReflectionTestUtils.setField(user, "updatedAt", LocalDateTime.of(2026, 3, 13, 9, 0));
|
||||
return user;
|
||||
}
|
||||
|
||||
private Role role(String code) {
|
||||
Role role = new Role();
|
||||
ReflectionTestUtils.setField(role, "code", code);
|
||||
ReflectionTestUtils.setField(role, "name", code);
|
||||
return role;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,9 +3,13 @@ package com.iflytek.skillhub.auth.repository;
|
|||
import com.iflytek.skillhub.auth.entity.UserRoleBinding;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface UserRoleBindingRepository extends JpaRepository<UserRoleBinding, Long> {
|
||||
List<UserRoleBinding> findByUserId(String userId);
|
||||
List<UserRoleBinding> findByUserIdIn(Collection<String> userIds);
|
||||
void deleteByUserId(String userId);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@ package com.iflytek.skillhub.infra.jpa;
|
|||
import com.iflytek.skillhub.domain.user.UserAccount;
|
||||
import com.iflytek.skillhub.domain.user.UserAccountRepository;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface UserAccountJpaRepository
|
||||
extends JpaRepository<UserAccount, String>, UserAccountRepository {
|
||||
extends JpaRepository<UserAccount, String>, JpaSpecificationExecutor<UserAccount>, UserAccountRepository {
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue