mirror of
https://github.com/iflytek/skillhub.git
synced 2026-08-27 11:14:59 +00:00
fix(review): allow super admin self promotion review
Signed-off-by: dongmucat <1127093059@qq.com>
This commit is contained in:
parent
4776550391
commit
256344cbde
10 changed files with 364 additions and 23 deletions
|
|
@ -112,7 +112,7 @@
|
|||
| skill owner | 可 | 可 | 不可 | 不可 | 可 | 不可 | 不可 | 不可 |
|
||||
| namespace ADMIN / OWNER | 可 | 可为本空间 skill 提交审核 | 可 | 不可 | 可 | 不可 | 不可 | 不可 |
|
||||
| SKILL_ADMIN | 可提交并可代提审;但普通发布仍非直发 | 可 | 可 | 可 | 可 | 可,但不能审自己的 promotion | 不可 | 可 |
|
||||
| SUPER_ADMIN | 可跨 namespace 发布且直接 `PUBLISHED`,跳过 membership 检查和 review task | 可 | 可 | 可 | 可 | 可;review 场景下还能审自己的提交 | 可 | 可 |
|
||||
| SUPER_ADMIN | 可跨 namespace 发布且直接 `PUBLISHED`,跳过 membership 检查和 review task | 可 | 可 | 可 | 可 | 可;promotion 和 review 场景下还能审自己的提交 | 可 | 可 |
|
||||
|
||||
### 对象存储写入策略
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ SkillHub 采用基于角色的访问控制(RBAC)系统。
|
|||
|
||||
| 角色 | 代码 | 实际能力 |
|
||||
|------|------|----------|
|
||||
| 超级管理员 | `SUPER_ADMIN` | 拥有全部权限;`RbacService#getUserPermissions` 会直接返回全部权限码;可访问所有 `SUPER_ADMIN`/`SKILL_ADMIN`/`USER_ADMIN`/`AUDITOR` 能访问的接口;可分配 `SUPER_ADMIN`;发布技能时可绕过命名空间成员校验并直接自动发布;但仍不能审批自己提交的 promotion,且普通审核单若是自己提交的,也只有 `SUPER_ADMIN` 能特判审批。 |
|
||||
| 超级管理员 | `SUPER_ADMIN` | 拥有全部权限;`RbacService#getUserPermissions` 会直接返回全部权限码;可访问所有 `SUPER_ADMIN`/`SKILL_ADMIN`/`USER_ADMIN`/`AUDITOR` 能访问的接口;可分配 `SUPER_ADMIN`;发布技能时可绕过命名空间成员校验并直接自动发布;可以审批自己提交的 promotion;普通审核单若是自己提交的,也只有 `SUPER_ADMIN` 能特判审批。 |
|
||||
| 技能管理员 | `SKILL_ADMIN` | 可访问技能治理后台接口;可隐藏/取消隐藏技能、撤回版本(yank)、处理技能举报;可查看和处理全局空间审核、promotion 审核、治理工作台收件箱中的 review/promotion/report;不能分配平台角色、不能看审计日志、不能管理用户。 |
|
||||
| 用户管理员 | `USER_ADMIN` | 可访问用户管理接口;可列表用户、审批用户、启用/禁用用户、修改平台角色;不能分配 `SUPER_ADMIN`;不能处理技能治理、不能看审计日志。 |
|
||||
| 审计员 | `AUDITOR` | 只读查看审计日志;可访问 `/api/v1/admin/audit-logs` 和 `/actuator/prometheus`;治理工作台中只能看 activity,不能处理 review/promotion/report,也不能管理用户或技能。 |
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ The database migration seeds only 4 explicit platform roles:
|
|||
|
||||
| Role | Code | Effective behavior |
|
||||
|------|------|--------------------|
|
||||
| Super Admin | `SUPER_ADMIN` | Has all permissions. `RbacService#getUserPermissions` returns all permission codes for this role. Can access all endpoints available to `SUPER_ADMIN` / `SKILL_ADMIN` / `USER_ADMIN` / `AUDITOR`. Can assign `SUPER_ADMIN`. Can bypass namespace membership checks during publish and auto-publish directly. Still cannot approve their own promotion request, and for normal review tasks the self-submission exception is only bypassed by `SUPER_ADMIN`. |
|
||||
| Super Admin | `SUPER_ADMIN` | Has all permissions. `RbacService#getUserPermissions` returns all permission codes for this role. Can access all endpoints available to `SUPER_ADMIN` / `SKILL_ADMIN` / `USER_ADMIN` / `AUDITOR`. Can assign `SUPER_ADMIN`. Can bypass namespace membership checks during publish and auto-publish directly. Can approve their own promotion request. For normal review tasks, the self-submission exception is also only bypassed by `SUPER_ADMIN`. |
|
||||
| Skill Admin | `SKILL_ADMIN` | Can access skill governance admin endpoints. Can hide/unhide skills, yank versions, and resolve/dismiss skill reports. Can review global namespace review tasks, promotion requests, and governance inbox items for review/promotion/report. Cannot manage users or read audit logs. |
|
||||
| User Admin | `USER_ADMIN` | Can access user management endpoints. Can list users, approve users, enable/disable users, and change platform roles. Cannot assign `SUPER_ADMIN`. Cannot perform skill governance or read audit logs. |
|
||||
| Auditor | `AUDITOR` | Read-only audit access. Can access `/api/v1/admin/audit-logs` and `/actuator/prometheus`. In the governance workbench this role can read activity, but cannot process review/promotion/report items and cannot manage users or skills. |
|
||||
|
|
|
|||
|
|
@ -75,7 +75,8 @@ public class PromotionPortalAppService {
|
|||
comment,
|
||||
platformRoles(userId)
|
||||
);
|
||||
recordAudit("PROMOTION_APPROVE", userId, promotion.getId(), auditContext, detailWithComment(comment));
|
||||
recordAudit("PROMOTION_APPROVE", userId, promotion.getId(), auditContext,
|
||||
detailWithComment(comment, promotion.getSubmittedBy().equals(userId)));
|
||||
return governanceQueryRepository.getPromotionResponse(promotion);
|
||||
}
|
||||
|
||||
|
|
@ -89,7 +90,8 @@ public class PromotionPortalAppService {
|
|||
comment,
|
||||
platformRoles(userId)
|
||||
);
|
||||
recordAudit("PROMOTION_REJECT", userId, promotion.getId(), auditContext, detailWithComment(comment));
|
||||
recordAudit("PROMOTION_REJECT", userId, promotion.getId(), auditContext,
|
||||
detailWithComment(comment, promotion.getSubmittedBy().equals(userId)));
|
||||
return governanceQueryRepository.getPromotionResponse(promotion);
|
||||
}
|
||||
|
||||
|
|
@ -159,10 +161,26 @@ public class PromotionPortalAppService {
|
|||
);
|
||||
}
|
||||
|
||||
private String detailWithComment(String comment) {
|
||||
if (comment == null || comment.isBlank()) {
|
||||
private String detailWithComment(String comment, boolean selfReview) {
|
||||
boolean hasComment = comment != null && !comment.isBlank();
|
||||
if (!hasComment && !selfReview) {
|
||||
return null;
|
||||
}
|
||||
return "{\"comment\":\"" + comment.replace("\"", "\\\"") + "\"}";
|
||||
StringBuilder detail = new StringBuilder("{");
|
||||
if (hasComment) {
|
||||
detail.append("\"comment\":\"").append(escapeJson(comment)).append("\"");
|
||||
}
|
||||
if (selfReview) {
|
||||
if (hasComment) {
|
||||
detail.append(",");
|
||||
}
|
||||
detail.append("\"selfReview\":true");
|
||||
}
|
||||
detail.append("}");
|
||||
return detail.toString();
|
||||
}
|
||||
|
||||
private String escapeJson(String value) {
|
||||
return value.replace("\\", "\\\\").replace("\"", "\\\"");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ import org.springframework.test.context.ActiveProfiles;
|
|||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
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;
|
||||
|
|
@ -53,6 +56,8 @@ class PromotionApprovalFlowIntegrationTest {
|
|||
|
||||
private static final String SUBMITTER_ID = "promotion-owner";
|
||||
private static final String REVIEWER_ID = "docker-admin";
|
||||
private static final String SELF_SUPER_ADMIN_ID = "self-super-admin";
|
||||
private static final String SELF_SKILL_ADMIN_ID = "self-skill-admin";
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
|
@ -127,31 +132,86 @@ class PromotionApprovalFlowIntegrationTest {
|
|||
assertThat(targetVersions.get(0).getStatus()).isEqualTo(SkillVersionStatus.PUBLISHED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void approvePromotion_allowsSuperAdminToApproveOwnPromotionThroughV1Route() throws Exception {
|
||||
when(rbacService.getUserRoleCodes(SELF_SUPER_ADMIN_ID)).thenReturn(Set.of("SUPER_ADMIN"));
|
||||
PromotionGraph graph = createPromotionGraph(SELF_SUPER_ADMIN_ID);
|
||||
|
||||
mockMvc.perform(post("/api/v1/promotions/" + graph.request().getId() + "/approve")
|
||||
.contentType("application/json")
|
||||
.content("{\"comment\":\"self approve\"}")
|
||||
.with(authentication(portalAuth(SELF_SUPER_ADMIN_ID, "SUPER_ADMIN")))
|
||||
.with(csrf()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.id").value(graph.request().getId()))
|
||||
.andExpect(jsonPath("$.data.status").value("APPROVED"))
|
||||
.andExpect(jsonPath("$.data.reviewedBy").value(SELF_SUPER_ADMIN_ID))
|
||||
.andExpect(jsonPath("$.data.submittedBy").value(SELF_SUPER_ADMIN_ID));
|
||||
|
||||
PromotionRequest savedRequest = promotionRequestRepository.findAllById(List.of(graph.request().getId()))
|
||||
.stream()
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
assertThat(savedRequest.getStatus()).isEqualTo(ReviewTaskStatus.APPROVED);
|
||||
assertThat(savedRequest.getReviewedBy()).isEqualTo(SELF_SUPER_ADMIN_ID);
|
||||
assertThat(savedRequest.getTargetSkillId()).isNotNull();
|
||||
verify(governanceNotificationService).notifyUser(
|
||||
eq(SELF_SUPER_ADMIN_ID),
|
||||
eq("PROMOTION"),
|
||||
eq("PROMOTION_REQUEST"),
|
||||
eq(graph.request().getId()),
|
||||
eq("Promotion approved"),
|
||||
any()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void approvePromotion_rejectsSkillAdminSelfApprovalThroughWebRoute() throws Exception {
|
||||
when(rbacService.getUserRoleCodes(SELF_SKILL_ADMIN_ID)).thenReturn(Set.of("SKILL_ADMIN"));
|
||||
PromotionGraph graph = createPromotionGraph(SELF_SKILL_ADMIN_ID);
|
||||
|
||||
mockMvc.perform(post("/api/web/promotions/" + graph.request().getId() + "/approve")
|
||||
.contentType("application/json")
|
||||
.content("{\"comment\":\"self approve\"}")
|
||||
.with(authentication(portalAuth(SELF_SKILL_ADMIN_ID, "SKILL_ADMIN")))
|
||||
.with(csrf()))
|
||||
.andExpect(status().isForbidden())
|
||||
.andExpect(jsonPath("$.code").value(403));
|
||||
|
||||
PromotionRequest savedRequest = promotionRequestRepository.findAllById(List.of(graph.request().getId()))
|
||||
.stream()
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
assertThat(savedRequest.getStatus()).isEqualTo(ReviewTaskStatus.PENDING);
|
||||
assertThat(savedRequest.getTargetSkillId()).isNull();
|
||||
}
|
||||
|
||||
private PromotionGraph createPromotionGraph() {
|
||||
return createPromotionGraph(SUBMITTER_ID);
|
||||
}
|
||||
|
||||
private PromotionGraph createPromotionGraph(String submitterId) {
|
||||
String suffix = UUID.randomUUID().toString().substring(0, 8);
|
||||
|
||||
userAccountRepository.saveAndFlush(
|
||||
new UserAccount(SUBMITTER_ID, "Promotion Owner", "owner-" + suffix + "@example.com", null)
|
||||
);
|
||||
userAccountRepository.saveAndFlush(
|
||||
new UserAccount(REVIEWER_ID, "Admin", "admin-" + suffix + "@example.com", null)
|
||||
);
|
||||
saveUserIfAbsent(submitterId, "Promotion Owner", "owner-" + suffix + "@example.com");
|
||||
saveUserIfAbsent(REVIEWER_ID, "Admin", "admin-" + suffix + "@example.com");
|
||||
|
||||
Namespace globalNamespace = new Namespace("global-" + suffix, "Global " + suffix, REVIEWER_ID);
|
||||
globalNamespace.setType(NamespaceType.GLOBAL);
|
||||
globalNamespace = namespaceRepository.saveAndFlush(globalNamespace);
|
||||
|
||||
Namespace teamNamespace = new Namespace("team-" + suffix, "Team " + suffix, SUBMITTER_ID);
|
||||
Namespace teamNamespace = new Namespace("team-" + suffix, "Team " + suffix, submitterId);
|
||||
teamNamespace = namespaceRepository.saveAndFlush(teamNamespace);
|
||||
|
||||
Skill sourceSkill = new Skill(teamNamespace.getId(), "promote-skill-" + suffix, SUBMITTER_ID, SkillVisibility.PUBLIC);
|
||||
Skill sourceSkill = new Skill(teamNamespace.getId(), "promote-skill-" + suffix, submitterId, SkillVisibility.PUBLIC);
|
||||
sourceSkill.setDisplayName("Promote Skill " + suffix);
|
||||
sourceSkill.setSummary("Used to verify promotion approval flow.");
|
||||
sourceSkill.setCreatedBy(SUBMITTER_ID);
|
||||
sourceSkill.setUpdatedBy(SUBMITTER_ID);
|
||||
sourceSkill.setCreatedBy(submitterId);
|
||||
sourceSkill.setUpdatedBy(submitterId);
|
||||
sourceSkill = skillRepository.saveAndFlush(sourceSkill);
|
||||
|
||||
SkillVersion sourceVersion = new SkillVersion(sourceSkill.getId(), "1.0.0", SUBMITTER_ID);
|
||||
SkillVersion sourceVersion = new SkillVersion(sourceSkill.getId(), "1.0.0", submitterId);
|
||||
sourceVersion.setStatus(SkillVersionStatus.PUBLISHED);
|
||||
sourceVersion.setPublishedAt(Instant.now());
|
||||
sourceVersion.setRequestedVisibility(SkillVisibility.PUBLIC);
|
||||
|
|
@ -160,16 +220,22 @@ class PromotionApprovalFlowIntegrationTest {
|
|||
sourceVersion = skillVersionRepository.saveAndFlush(sourceVersion);
|
||||
|
||||
sourceSkill.setLatestVersionId(sourceVersion.getId());
|
||||
sourceSkill.setUpdatedBy(SUBMITTER_ID);
|
||||
sourceSkill.setUpdatedBy(submitterId);
|
||||
sourceSkill = skillRepository.saveAndFlush(sourceSkill);
|
||||
|
||||
PromotionRequest request = promotionRequestRepository.saveAndFlush(
|
||||
new PromotionRequest(sourceSkill.getId(), sourceVersion.getId(), globalNamespace.getId(), SUBMITTER_ID)
|
||||
new PromotionRequest(sourceSkill.getId(), sourceVersion.getId(), globalNamespace.getId(), submitterId)
|
||||
);
|
||||
|
||||
return new PromotionGraph(globalNamespace, sourceSkill, sourceVersion, request);
|
||||
}
|
||||
|
||||
private void saveUserIfAbsent(String userId, String displayName, String email) {
|
||||
if (!userAccountRepository.existsById(userId)) {
|
||||
userAccountRepository.saveAndFlush(new UserAccount(userId, displayName, email, null));
|
||||
}
|
||||
}
|
||||
|
||||
private UsernamePasswordAuthenticationToken portalAuth(String userId, String... roles) {
|
||||
PlatformPrincipal principal = new PlatformPrincipal(
|
||||
userId,
|
||||
|
|
|
|||
|
|
@ -144,6 +144,32 @@ class NotificationEventListenerTest {
|
|||
eq("PROMOTION_SUBMITTED"), anyString(), anyString(), eq("PROMOTION"), eq(200L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void onPromotionApproved_shouldDispatchToSubmitterWhenReviewerIsSubmitter() throws Exception {
|
||||
Skill skill = mockSkill(1L);
|
||||
when(skillRepository.findById(1L)).thenReturn(Optional.of(skill));
|
||||
mockNamespace();
|
||||
when(objectMapper.writeValueAsString(any())).thenReturn("{}");
|
||||
|
||||
listener.onPromotionApproved(new PromotionApprovedEvent(200L, 1L, "self-admin", "self-admin"));
|
||||
|
||||
verify(dispatcher).dispatch(eq("self-admin"), eq(NotificationCategory.PROMOTION),
|
||||
eq("PROMOTION_APPROVED"), anyString(), anyString(), eq("SKILL"), eq(1L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void onPromotionRejected_shouldDispatchToSubmitterWhenReviewerIsSubmitter() throws Exception {
|
||||
Skill skill = mockSkill(1L);
|
||||
when(skillRepository.findById(1L)).thenReturn(Optional.of(skill));
|
||||
mockNamespace();
|
||||
when(objectMapper.writeValueAsString(any())).thenReturn("{}");
|
||||
|
||||
listener.onPromotionRejected(new PromotionRejectedEvent(200L, 1L, "self-admin", "self-admin", "not ready"));
|
||||
|
||||
verify(dispatcher).dispatch(eq("self-admin"), eq(NotificationCategory.PROMOTION),
|
||||
eq("PROMOTION_REJECTED"), anyString(), anyString(), eq("SKILL"), eq(1L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void onReportResolved_shouldDispatchToReporter() throws Exception {
|
||||
Skill skill = mockSkill(1L);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,170 @@
|
|||
package com.iflytek.skillhub.service;
|
||||
|
||||
import com.iflytek.skillhub.auth.rbac.RbacService;
|
||||
import com.iflytek.skillhub.domain.audit.AuditLogService;
|
||||
import com.iflytek.skillhub.domain.review.PromotionRequest;
|
||||
import com.iflytek.skillhub.domain.review.PromotionRequestRepository;
|
||||
import com.iflytek.skillhub.domain.review.PromotionService;
|
||||
import com.iflytek.skillhub.dto.PromotionResponseDto;
|
||||
import com.iflytek.skillhub.repository.GovernanceQueryRepository;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PromotionPortalAppServiceTest {
|
||||
|
||||
private static final Long PROMOTION_ID = 1L;
|
||||
private static final String SUPER_ADMIN_ID = "super-admin";
|
||||
private static final String REVIEWER_ID = "reviewer";
|
||||
private static final String SUBMITTER_ID = "submitter";
|
||||
|
||||
@Mock
|
||||
private PromotionService promotionService;
|
||||
@Mock
|
||||
private PromotionRequestRepository promotionRequestRepository;
|
||||
@Mock
|
||||
private GovernanceQueryRepository governanceQueryRepository;
|
||||
@Mock
|
||||
private RbacService rbacService;
|
||||
@Mock
|
||||
private AuditLogService auditLogService;
|
||||
|
||||
private PromotionPortalAppService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new PromotionPortalAppService(
|
||||
promotionService,
|
||||
promotionRequestRepository,
|
||||
governanceQueryRepository,
|
||||
rbacService,
|
||||
auditLogService
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void approvePromotion_recordsSelfReviewAuditDetailForSuperAdminSelfApproval() {
|
||||
PromotionRequest promotion = promotionRequest(PROMOTION_ID, SUPER_ADMIN_ID);
|
||||
when(rbacService.getUserRoleCodes(SUPER_ADMIN_ID)).thenReturn(Set.of("SUPER_ADMIN"));
|
||||
when(promotionService.approvePromotion(PROMOTION_ID, SUPER_ADMIN_ID, "ship", Set.of("SUPER_ADMIN")))
|
||||
.thenReturn(promotion);
|
||||
when(governanceQueryRepository.getPromotionResponse(promotion)).thenReturn(response(promotion));
|
||||
|
||||
service.approvePromotion(
|
||||
PROMOTION_ID,
|
||||
"ship",
|
||||
SUPER_ADMIN_ID,
|
||||
new AuditRequestContext("127.0.0.1", "JUnit")
|
||||
);
|
||||
|
||||
verify(auditLogService).record(
|
||||
eq(SUPER_ADMIN_ID),
|
||||
eq("PROMOTION_APPROVE"),
|
||||
eq("PROMOTION_REQUEST"),
|
||||
eq(PROMOTION_ID),
|
||||
eq(null),
|
||||
eq("127.0.0.1"),
|
||||
eq("JUnit"),
|
||||
eq("{\"comment\":\"ship\",\"selfReview\":true}")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectPromotion_recordsSelfReviewAuditDetailWithoutComment() {
|
||||
PromotionRequest promotion = promotionRequest(PROMOTION_ID, SUPER_ADMIN_ID);
|
||||
when(rbacService.getUserRoleCodes(SUPER_ADMIN_ID)).thenReturn(Set.of("SUPER_ADMIN"));
|
||||
when(promotionService.rejectPromotion(PROMOTION_ID, SUPER_ADMIN_ID, null, Set.of("SUPER_ADMIN")))
|
||||
.thenReturn(promotion);
|
||||
when(governanceQueryRepository.getPromotionResponse(promotion)).thenReturn(response(promotion));
|
||||
|
||||
service.rejectPromotion(
|
||||
PROMOTION_ID,
|
||||
null,
|
||||
SUPER_ADMIN_ID,
|
||||
new AuditRequestContext("127.0.0.1", "JUnit")
|
||||
);
|
||||
|
||||
verify(auditLogService).record(
|
||||
eq(SUPER_ADMIN_ID),
|
||||
eq("PROMOTION_REJECT"),
|
||||
eq("PROMOTION_REQUEST"),
|
||||
eq(PROMOTION_ID),
|
||||
eq(null),
|
||||
eq("127.0.0.1"),
|
||||
eq("JUnit"),
|
||||
eq("{\"selfReview\":true}")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void approvePromotion_keepsExistingAuditDetailForReviewerApprovingOthersPromotion() {
|
||||
PromotionRequest promotion = promotionRequest(PROMOTION_ID, SUBMITTER_ID);
|
||||
when(rbacService.getUserRoleCodes(REVIEWER_ID)).thenReturn(Set.of("SKILL_ADMIN"));
|
||||
when(promotionService.approvePromotion(PROMOTION_ID, REVIEWER_ID, "ship", Set.of("SKILL_ADMIN")))
|
||||
.thenReturn(promotion);
|
||||
when(governanceQueryRepository.getPromotionResponse(promotion)).thenReturn(response(promotion));
|
||||
|
||||
service.approvePromotion(
|
||||
PROMOTION_ID,
|
||||
"ship",
|
||||
REVIEWER_ID,
|
||||
new AuditRequestContext("127.0.0.1", "JUnit")
|
||||
);
|
||||
|
||||
verify(auditLogService).record(
|
||||
eq(REVIEWER_ID),
|
||||
eq("PROMOTION_APPROVE"),
|
||||
eq("PROMOTION_REQUEST"),
|
||||
eq(PROMOTION_ID),
|
||||
eq(null),
|
||||
eq("127.0.0.1"),
|
||||
eq("JUnit"),
|
||||
eq("{\"comment\":\"ship\"}")
|
||||
);
|
||||
}
|
||||
|
||||
private PromotionResponseDto response(PromotionRequest request) {
|
||||
return new PromotionResponseDto(
|
||||
request.getId(),
|
||||
request.getSourceSkillId(),
|
||||
"team-a",
|
||||
"skill-a",
|
||||
"1.0.0",
|
||||
"global",
|
||||
request.getTargetSkillId(),
|
||||
request.getStatus().name(),
|
||||
request.getSubmittedBy(),
|
||||
"Submitter",
|
||||
request.getReviewedBy(),
|
||||
null,
|
||||
request.getReviewComment(),
|
||||
request.getSubmittedAt(),
|
||||
request.getReviewedAt()
|
||||
);
|
||||
}
|
||||
|
||||
private PromotionRequest promotionRequest(Long id, String submittedBy) {
|
||||
PromotionRequest request = new PromotionRequest(10L, 20L, 30L, submittedBy);
|
||||
setId(request, id);
|
||||
return request;
|
||||
}
|
||||
|
||||
private void setId(Object entity, Long id) {
|
||||
try {
|
||||
Field idField = entity.getClass().getDeclaredField("id");
|
||||
idField.setAccessible(true);
|
||||
idField.set(entity, id);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -108,7 +108,7 @@ public class ReviewPermissionChecker {
|
|||
String userId,
|
||||
Set<String> platformRoles) {
|
||||
if (request.getSubmittedBy().equals(userId)) {
|
||||
return false;
|
||||
return platformRoles.contains("SUPER_ADMIN");
|
||||
}
|
||||
return hasPlatformReviewRole(platformRoles);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -401,6 +401,51 @@ class PromotionServiceTest {
|
|||
|
||||
verify(governanceNotificationService).notifyUser(eq(USER_ID), eq("PROMOTION"), eq("PROMOTION_REQUEST"), eq(PROMOTION_ID), eq("Promotion rejected"), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotifySubmitterWhenSuperAdminApprovesOwnPromotion() {
|
||||
PromotionRequest request = createPendingPromotion();
|
||||
PromotionRequest approvedRequest = approvedPromotion(request, "self approve");
|
||||
approvedRequest.setReviewedBy(USER_ID);
|
||||
Skill sourceSkill = createSourceSkill();
|
||||
SkillVersion sourceVersion = createPublishedVersion();
|
||||
Skill newSkill = new Skill(TARGET_NAMESPACE_ID, "my-skill", USER_ID, SkillVisibility.PUBLIC);
|
||||
setField(newSkill, "id", NEW_SKILL_ID);
|
||||
SkillVersion newVersion = new SkillVersion(NEW_SKILL_ID, sourceVersion.getVersion(), USER_ID);
|
||||
setField(newVersion, "id", NEW_VERSION_ID);
|
||||
|
||||
when(promotionRequestRepository.findById(PROMOTION_ID))
|
||||
.thenReturn(Optional.of(request), Optional.of(approvedRequest));
|
||||
when(permissionChecker.canReviewPromotion(request, USER_ID, Set.of("SUPER_ADMIN"))).thenReturn(true);
|
||||
when(promotionRequestRepository.updateStatusWithVersion(
|
||||
PROMOTION_ID, ReviewTaskStatus.APPROVED, USER_ID, "self approve", null, request.getVersion()))
|
||||
.thenReturn(1);
|
||||
when(skillRepository.findById(SOURCE_SKILL_ID)).thenReturn(Optional.of(sourceSkill));
|
||||
when(skillVersionRepository.findById(SOURCE_VERSION_ID)).thenReturn(Optional.of(sourceVersion));
|
||||
when(skillRepository.save(any(Skill.class))).thenReturn(newSkill);
|
||||
when(skillVersionRepository.save(any(SkillVersion.class))).thenReturn(newVersion);
|
||||
when(skillFileRepository.findByVersionId(SOURCE_VERSION_ID)).thenReturn(List.of());
|
||||
when(promotionRequestRepository.save(approvedRequest)).thenReturn(approvedRequest);
|
||||
|
||||
promotionService.approvePromotion(PROMOTION_ID, USER_ID, "self approve", Set.of("SUPER_ADMIN"));
|
||||
|
||||
verify(governanceNotificationService).notifyUser(eq(USER_ID), eq("PROMOTION"), eq("PROMOTION_REQUEST"), eq(PROMOTION_ID), eq("Promotion approved"), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotifySubmitterWhenSuperAdminRejectsOwnPromotion() {
|
||||
PromotionRequest request = createPendingPromotion();
|
||||
|
||||
when(promotionRequestRepository.findById(PROMOTION_ID)).thenReturn(Optional.of(request));
|
||||
when(permissionChecker.canReviewPromotion(request, USER_ID, Set.of("SUPER_ADMIN"))).thenReturn(true);
|
||||
when(promotionRequestRepository.updateStatusWithVersion(
|
||||
PROMOTION_ID, ReviewTaskStatus.REJECTED, USER_ID, "self reject", null, request.getVersion()))
|
||||
.thenReturn(1);
|
||||
|
||||
promotionService.rejectPromotion(PROMOTION_ID, USER_ID, "self reject", Set.of("SUPER_ADMIN"));
|
||||
|
||||
verify(governanceNotificationService).notifyUser(eq(USER_ID), eq("PROMOTION"), eq("PROMOTION_REQUEST"), eq(PROMOTION_ID), eq("Promotion rejected"), any());
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
|
|
|
|||
|
|
@ -234,10 +234,26 @@ class ReviewPermissionCheckerTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
void cannotReviewOwnPromotion() {
|
||||
void skillAdminCannotReviewOwnPromotion() {
|
||||
String userId = "user-2";
|
||||
PromotionRequest req = new PromotionRequest(1L, 1L, 1L, userId);
|
||||
assertFalse(checker.canReviewPromotion(req, userId,
|
||||
Set.of("SKILL_ADMIN")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void regularUserCannotReviewOwnPromotion() {
|
||||
String userId = "user-2";
|
||||
PromotionRequest req = new PromotionRequest(1L, 1L, 1L, userId);
|
||||
assertFalse(checker.canReviewPromotion(req, userId,
|
||||
Set.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void superAdminCanReviewOwnPromotion() {
|
||||
String userId = "user-2";
|
||||
PromotionRequest req = new PromotionRequest(1L, 1L, 1L, userId);
|
||||
assertTrue(checker.canReviewPromotion(req, userId,
|
||||
Set.of("SUPER_ADMIN")));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue