mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-06 08:15:57 +00:00
fix(governance): make mutations and audit atomic
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
parent
13510609a0
commit
b22b92fcbc
4 changed files with 68 additions and 0 deletions
|
|
@ -24,6 +24,7 @@ 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;
|
||||
|
||||
@Service
|
||||
public class PromotionPortalAppService {
|
||||
|
|
@ -49,6 +50,7 @@ public class PromotionPortalAppService {
|
|||
this.requestIdAccessor = requestIdAccessor;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PromotionResponseDto submitPromotion(Long sourceSkillId,
|
||||
Long sourceVersionId,
|
||||
Long targetNamespaceId,
|
||||
|
|
@ -73,6 +75,7 @@ public class PromotionPortalAppService {
|
|||
return governanceQueryRepository.getPromotionResponse(promotion);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PromotionResponseDto approvePromotion(Long promotionId,
|
||||
String comment,
|
||||
String userId,
|
||||
|
|
@ -88,6 +91,7 @@ public class PromotionPortalAppService {
|
|||
return governanceQueryRepository.getPromotionResponse(promotion);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PromotionResponseDto rejectPromotion(Long promotionId,
|
||||
String comment,
|
||||
String userId,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import org.springframework.data.domain.Pageable;
|
|||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
public class ReviewPortalAppService {
|
||||
|
|
@ -53,6 +54,7 @@ public class ReviewPortalAppService {
|
|||
this.requestIdAccessor = requestIdAccessor;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ReviewTaskResponse submitReview(Long skillVersionId,
|
||||
String userId,
|
||||
Map<Long, NamespaceRole> userNsRoles,
|
||||
|
|
@ -67,6 +69,7 @@ public class ReviewPortalAppService {
|
|||
return governanceQueryRepository.getReviewTaskResponse(task);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ReviewTaskResponse approveReview(Long reviewTaskId,
|
||||
String comment,
|
||||
String userId,
|
||||
|
|
@ -83,6 +86,7 @@ public class ReviewPortalAppService {
|
|||
return governanceQueryRepository.getReviewTaskResponse(task);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ReviewTaskResponse rejectReview(Long reviewTaskId,
|
||||
String comment,
|
||||
String userId,
|
||||
|
|
@ -99,6 +103,7 @@ public class ReviewPortalAppService {
|
|||
return governanceQueryRepository.getReviewTaskResponse(task);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void withdrawReview(Long reviewTaskId,
|
||||
String userId,
|
||||
AuditRequestContext auditContext) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.iflytek.skillhub.TestRedisConfig;
|
|||
import com.iflytek.skillhub.auth.device.DeviceAuthService;
|
||||
import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
|
||||
import com.iflytek.skillhub.auth.rbac.RbacService;
|
||||
import com.iflytek.skillhub.domain.audit.AuditLogRepository;
|
||||
import com.iflytek.skillhub.domain.governance.GovernanceNotificationService;
|
||||
import com.iflytek.skillhub.domain.namespace.Namespace;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceType;
|
||||
|
|
@ -32,6 +33,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock
|
|||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
|
@ -91,6 +93,9 @@ class PromotionApprovalFlowIntegrationTest {
|
|||
@MockBean
|
||||
private NotificationDispatcher notificationDispatcher;
|
||||
|
||||
@MockBean
|
||||
private AuditLogRepository auditLogRepository;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
when(rbacService.getUserRoleCodes(REVIEWER_ID)).thenReturn(Set.of("SUPER_ADMIN"));
|
||||
|
|
@ -195,6 +200,29 @@ class PromotionApprovalFlowIntegrationTest {
|
|||
assertThat(savedRequest.getTargetSkillId()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void approvePromotion_rollsBackTargetCopyWhenAuditPersistenceFails() throws Exception {
|
||||
PromotionGraph graph = createPromotionGraph();
|
||||
when(auditLogRepository.save(any()))
|
||||
.thenThrow(new DataIntegrityViolationException("forced audit failure"));
|
||||
|
||||
mockMvc.perform(post("/api/web/promotions/" + graph.request().getId() + "/approve")
|
||||
.contentType("application/json")
|
||||
.content("{\"comment\":\"ship it\"}")
|
||||
.with(authentication(portalAuth(REVIEWER_ID, "SUPER_ADMIN")))
|
||||
.with(csrf()))
|
||||
.andExpect(status().isInternalServerError());
|
||||
|
||||
PromotionRequest savedRequest = promotionRequestRepository.findAllById(List.of(graph.request().getId()))
|
||||
.stream()
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
assertThat(savedRequest.getStatus()).isEqualTo(ReviewTaskStatus.PENDING);
|
||||
assertThat(savedRequest.getTargetSkillId()).isNull();
|
||||
assertThat(skillRepository.findByNamespaceIdAndSlug(
|
||||
graph.globalNamespace().getId(), graph.sourceSkill().getSlug())).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Transactional
|
||||
void listPromotions_sortsApprovedAndRejectedHistoryByReviewedAtWithNullsLastAndTieBreaker() throws Exception {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import com.iflytek.skillhub.TestRedisConfig;
|
|||
import com.iflytek.skillhub.auth.device.DeviceAuthService;
|
||||
import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
|
||||
import com.iflytek.skillhub.auth.rbac.RbacService;
|
||||
import com.iflytek.skillhub.domain.audit.AuditLogRepository;
|
||||
import com.iflytek.skillhub.domain.namespace.Namespace;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository;
|
||||
import com.iflytek.skillhub.domain.namespace.NamespaceRole;
|
||||
|
|
@ -34,6 +35,7 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock
|
|||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
|
@ -41,6 +43,7 @@ import org.springframework.test.web.servlet.MockMvc;
|
|||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
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;
|
||||
|
|
@ -84,6 +87,9 @@ class SkillApprovalVisibilityFlowIntegrationTest {
|
|||
@MockBean
|
||||
private RbacService rbacService;
|
||||
|
||||
@MockBean
|
||||
private AuditLogRepository auditLogRepository;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
when(searchEmbeddingService.embed(anyString())).thenReturn("");
|
||||
|
|
@ -160,6 +166,31 @@ class SkillApprovalVisibilityFlowIntegrationTest {
|
|||
assertThat(savedVersion.getPublishedAt()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void approveReview_rollsBackDomainStateWhenAuditPersistenceFails() throws Exception {
|
||||
PendingSkillGraph graph = createPendingGlobalSkill("local-user");
|
||||
when(auditLogRepository.save(any()))
|
||||
.thenThrow(new DataIntegrityViolationException("forced audit failure"));
|
||||
|
||||
mockMvc.perform(post("/api/v1/reviews/" + graph.reviewTask().getId() + "/approve")
|
||||
.contentType("application/json")
|
||||
.content("{\"comment\":\"ship it\"}")
|
||||
.with(authentication(apiAuth("super-1", "SUPER_ADMIN")))
|
||||
.with(csrf()))
|
||||
.andExpect(status().isInternalServerError());
|
||||
|
||||
ReviewTask savedReviewTask = reviewTaskJpaRepository.findAllById(List.of(graph.reviewTask().getId()))
|
||||
.stream()
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
assertThat(savedReviewTask.getStatus())
|
||||
.isEqualTo(com.iflytek.skillhub.domain.review.ReviewTaskStatus.PENDING);
|
||||
assertThat(skillVersionRepository.findById(graph.version().getId()).orElseThrow().getStatus())
|
||||
.isEqualTo(SkillVersionStatus.PENDING_REVIEW);
|
||||
assertThat(skillRepository.findById(graph.skill().getId()).orElseThrow().getLatestVersionId()).isNull();
|
||||
assertThat(skillSearchDocumentJpaRepository.findBySkillId(graph.skill().getId())).isEmpty();
|
||||
}
|
||||
|
||||
private PendingSkillGraph createPendingGlobalSkill(String ownerId) {
|
||||
String suffix = UUID.randomUUID().toString().substring(0, 8);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue