From 1e2de2798b9c04c01096adeddafddab4f7e3af32 Mon Sep 17 00:00:00 2001 From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:14:16 +0800 Subject: [PATCH] test(auth): verify organization creation rollback Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --- .../tasks.md | 4 +- .../PlatformOrganizationControllerTest.java | 50 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/openspec/changes/enterprise-identity-r1b-org-create/tasks.md b/openspec/changes/enterprise-identity-r1b-org-create/tasks.md index 03e00d80..a06833c6 100644 --- a/openspec/changes/enterprise-identity-r1b-org-create/tasks.md +++ b/openspec/changes/enterprise-identity-r1b-org-create/tasks.md @@ -6,6 +6,6 @@ ## Verification -- [x] 集成测试覆盖成功、仅 owner 可进入组织详情、非管理员、禁用/不存在 owner 和重复 slug。 +- [x] 集成测试覆盖成功、仅 owner 可进入组织详情、非管理员、禁用/不存在 owner、重复 slug,以及审计失败时四类写入的事务回滚。 - [x] 完整后端测试、Web typecheck/lint 和 OpenSpec strict validation 通过。 -- [ ] 检查最终 diff、敏感信息、精确 SHA 与预览环境;经人工评审后再提 PR/合并。 +- [ ] 核对 PR 最终 diff、敏感信息、精确 SHA 与预览环境;完成评审和合并前验收。 diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/admin/PlatformOrganizationControllerTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/admin/PlatformOrganizationControllerTest.java index fcc96974..8465929b 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/admin/PlatformOrganizationControllerTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/admin/PlatformOrganizationControllerTest.java @@ -1,6 +1,11 @@ package com.iflytek.skillhub.controller.admin; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; 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; @@ -10,6 +15,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import com.iflytek.skillhub.auth.device.DeviceAuthService; import com.iflytek.skillhub.auth.rbac.PlatformPrincipal; +import com.iflytek.skillhub.domain.audit.AuditLogService; import com.iflytek.skillhub.domain.audit.AuditLog; import com.iflytek.skillhub.domain.organization.Organization; import com.iflytek.skillhub.domain.organization.OrganizationMembership; @@ -17,6 +23,8 @@ import com.iflytek.skillhub.domain.organization.OrganizationRole; import com.iflytek.skillhub.domain.organization.OrganizationRoleBinding; import com.iflytek.skillhub.domain.user.UserAccount; import com.iflytek.skillhub.domain.user.UserStatus; +import com.iflytek.skillhub.dto.OrganizationCreateRequest; +import com.iflytek.skillhub.service.PlatformOrganizationCreateAppService; import jakarta.persistence.EntityManager; import java.nio.file.Files; import java.nio.file.Path; @@ -27,12 +35,15 @@ 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.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.http.MediaType; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.support.TransactionTemplate; @SpringBootTest @AutoConfigureMockMvc @@ -46,6 +57,15 @@ class PlatformOrganizationControllerTest { @Autowired private EntityManager entityManager; + @Autowired + private TransactionTemplate transactionTemplate; + + @Autowired + private PlatformOrganizationCreateAppService appService; + + @SpyBean + private AuditLogService auditLogService; + @MockBean private DeviceAuthService deviceAuthService; @@ -185,6 +205,31 @@ class PlatformOrganizationControllerTest { assertThat(organizationCount()).isEqualTo(1); } + @Test + @Transactional(propagation = Propagation.NOT_SUPPORTED) + void create_rollsBackAllWritesWhenAuditFails() { + transactionTemplate.executeWithoutResult(status -> persistUser("rollback-owner")); + long organizationsBefore = count("Organization"); + long membershipsBefore = count("OrganizationMembership"); + long rolesBefore = count("OrganizationRoleBinding"); + long auditsBefore = count("AuditLog"); + + doThrow(new IllegalStateException("forced audit failure")) + .when(auditLogService).recordOrganizationCreated( + eq("platform-admin"), anyString(), any()); + + assertThatThrownBy(() -> appService.create( + new OrganizationCreateRequest("rollback-team", "Rollback Team", "rollback-owner"), + "platform-admin")) + .isInstanceOf(IllegalStateException.class) + .hasMessage("forced audit failure"); + + assertThat(count("Organization")).isEqualTo(organizationsBefore); + assertThat(count("OrganizationMembership")).isEqualTo(membershipsBefore); + assertThat(count("OrganizationRoleBinding")).isEqualTo(rolesBefore); + assertThat(count("AuditLog")).isEqualTo(auditsBefore); + } + private UserAccount persistUser(String userId) { UserAccount user = new UserAccount(userId, userId, userId + "@example.com", ""); entityManager.persist(user); @@ -198,6 +243,11 @@ class PlatformOrganizationControllerTest { .getSingleResult(); } + private long count(String entityName) { + return entityManager.createQuery("select count(e) from " + entityName + " e", Long.class) + .getSingleResult(); + } + private String request(String slug, String displayName, String owner) { return "{\"slug\":\"" + slug + "\",\"displayName\":\"" + displayName + "\",\"initialOwnerUserId\":\"" + owner + "\"}";