From e52853c49c7efe4760684941cfdfacf71ca1eb61 Mon Sep 17 00:00:00 2001 From: vsxd Date: Tue, 17 Mar 2026 14:22:26 +0800 Subject: [PATCH] test: cover role and user status permission edges --- .../admin/AdminSkillControllerTest.java | 14 +++ .../admin/AdminSkillReportControllerTest.java | 16 ++++ .../identity/IdentityBindingServiceTest.java | 88 +++++++++++++++++++ .../auth/local/LocalAuthServiceTest.java | 43 +++++++++ .../namespace/NamespaceAccessPolicyTest.java | 57 ++++++++++++ 5 files changed, 218 insertions(+) create mode 100644 server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/namespace/NamespaceAccessPolicyTest.java diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/admin/AdminSkillControllerTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/admin/AdminSkillControllerTest.java index 16c7c43b..8901d105 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/admin/AdminSkillControllerTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/admin/AdminSkillControllerTest.java @@ -86,4 +86,18 @@ class AdminSkillControllerTest { .andExpect(jsonPath("$.data.action").value("YANK")) .andExpect(jsonPath("$.data.status").value("YANKED")); } + + @Test + void hideSkill_withUserAdminRole_returns403() throws Exception { + PlatformPrincipal principal = new PlatformPrincipal("admin", "admin", "a@example.com", "", "github", Set.of("USER_ADMIN")); + var auth = new UsernamePasswordAuthenticationToken(principal, null, List.of(new SimpleGrantedAuthority("ROLE_USER_ADMIN"))); + + mockMvc.perform(post("/api/v1/admin/skills/10/hide") + .with(authentication(auth)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"reason\":\"policy\"}")) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.code").value(403)); + } } diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/admin/AdminSkillReportControllerTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/admin/AdminSkillReportControllerTest.java index 193c3126..1f3cf509 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/admin/AdminSkillReportControllerTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/admin/AdminSkillReportControllerTest.java @@ -112,6 +112,22 @@ class AdminSkillReportControllerTest { .andExpect(jsonPath("$.data.status").value("RESOLVED")); } + @Test + void listReports_withAuditorRole_returns403() throws Exception { + PlatformPrincipal principal = new PlatformPrincipal( + "auditor", "auditor", "auditor@example.com", "", "github", Set.of("AUDITOR") + ); + var auth = new UsernamePasswordAuthenticationToken( + principal, null, List.of(new SimpleGrantedAuthority("ROLE_AUDITOR")) + ); + + mockMvc.perform(get("/api/v1/admin/skill-reports") + .param("status", "PENDING") + .with(authentication(auth))) + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.code").value(403)); + } + private UsernamePasswordAuthenticationToken adminAuth() { PlatformPrincipal principal = new PlatformPrincipal( "admin", "admin", "admin@example.com", "", "github", Set.of("SKILL_ADMIN") diff --git a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/identity/IdentityBindingServiceTest.java b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/identity/IdentityBindingServiceTest.java index f3d4d659..79e5df74 100644 --- a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/identity/IdentityBindingServiceTest.java +++ b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/identity/IdentityBindingServiceTest.java @@ -8,6 +8,9 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.iflytek.skillhub.auth.entity.IdentityBinding; +import com.iflytek.skillhub.auth.entity.Role; +import com.iflytek.skillhub.auth.entity.UserRoleBinding; +import com.iflytek.skillhub.auth.oauth.AccountDisabledException; import com.iflytek.skillhub.auth.oauth.OAuthClaims; import com.iflytek.skillhub.auth.oauth.AccountPendingException; import com.iflytek.skillhub.auth.rbac.PlatformPrincipal; @@ -26,6 +29,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; @ExtendWith(MockitoExtension.class) class IdentityBindingServiceTest { @@ -91,4 +95,88 @@ class IdentityBindingServiceTest { verify(globalNamespaceMembershipService, never()).ensureMember(any()); } + + @Test + void bindOrCreate_defaultsToUserRoleWhenNoBindingsExist() { + OAuthClaims claims = new OAuthClaims( + "github", + "gh_1", + "alice@example.com", + true, + "alice", + Map.of() + ); + when(bindingRepo.findByProviderCodeAndSubject("github", "gh_1")).thenReturn(Optional.empty()); + when(userRepo.save(any(UserAccount.class))).thenAnswer(invocation -> invocation.getArgument(0)); + when(roleBindingRepo.findByUserId(any())).thenReturn(List.of()); + + PlatformPrincipal principal = service.bindOrCreate(claims, UserStatus.ACTIVE); + + assertThat(principal.platformRoles()).containsExactly("USER"); + } + + @Test + void bindOrCreate_existingDisabledUser_throwsAccountDisabled() { + OAuthClaims claims = new OAuthClaims( + "github", + "gh_1", + "alice@example.com", + true, + "alice", + Map.of() + ); + IdentityBinding binding = new IdentityBinding("usr_1", "github", "gh_1", "alice"); + UserAccount user = new UserAccount("usr_1", "alice", "alice@example.com", null); + user.setStatus(UserStatus.DISABLED); + + when(bindingRepo.findByProviderCodeAndSubject("github", "gh_1")).thenReturn(Optional.of(binding)); + when(userRepo.findById("usr_1")).thenReturn(Optional.of(user)); + when(userRepo.save(any(UserAccount.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + assertThatThrownBy(() -> service.bindOrCreate(claims, UserStatus.ACTIVE)) + .isInstanceOf(AccountDisabledException.class); + } + + @Test + void bindOrCreate_returnsExplicitPlatformRolesWhenBindingsExist() { + OAuthClaims claims = new OAuthClaims( + "github", + "gh_1", + "alice@example.com", + true, + "alice", + Map.of() + ); + when(bindingRepo.findByProviderCodeAndSubject("github", "gh_1")).thenReturn(Optional.empty()); + when(userRepo.save(any(UserAccount.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + Role role = new Role(); + ReflectionTestUtils.setField(role, "code", "AUDITOR"); + when(roleBindingRepo.findByUserId(any())).thenReturn(List.of(new UserRoleBinding("usr_1", role))); + + PlatformPrincipal principal = service.bindOrCreate(claims, UserStatus.ACTIVE); + + assertThat(principal.platformRoles()).containsExactly("AUDITOR"); + } + + @Test + void createPendingUserIfAbsent_existingDisabledBinding_throwsAccountDisabled() { + OAuthClaims claims = new OAuthClaims( + "github", + "gh_1", + "alice@example.com", + true, + "alice", + Map.of() + ); + IdentityBinding binding = new IdentityBinding("usr_1", "github", "gh_1", "alice"); + UserAccount user = new UserAccount("usr_1", "alice", "alice@example.com", null); + user.setStatus(UserStatus.DISABLED); + + when(bindingRepo.findByProviderCodeAndSubject("github", "gh_1")).thenReturn(Optional.of(binding)); + when(userRepo.findById("usr_1")).thenReturn(Optional.of(user)); + + assertThatThrownBy(() -> service.createPendingUserIfAbsent(claims)) + .isInstanceOf(AccountDisabledException.class); + } } diff --git a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/local/LocalAuthServiceTest.java b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/local/LocalAuthServiceTest.java index 42b5cce9..3b069463 100644 --- a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/local/LocalAuthServiceTest.java +++ b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/local/LocalAuthServiceTest.java @@ -150,6 +150,49 @@ class LocalAuthServiceTest { .hasMessageContaining("error.auth.local.accountDisabled"); } + @Test + void login_withPendingAccount_fails() { + LocalCredential credential = new LocalCredential("usr_1", "alice", "encoded"); + UserAccount user = new UserAccount("usr_1", "alice", "alice@example.com", null); + user.setStatus(UserStatus.PENDING); + + given(credentialRepository.findByUsernameIgnoreCase("alice")).willReturn(Optional.of(credential)); + given(userAccountRepository.findById("usr_1")).willReturn(Optional.of(user)); + + assertThatThrownBy(() -> service.login("alice", "Abcd123!")) + .isInstanceOf(AuthFlowException.class) + .hasMessageContaining("error.auth.local.accountPending"); + } + + @Test + void login_withMergedAccount_fails() { + LocalCredential credential = new LocalCredential("usr_1", "alice", "encoded"); + UserAccount user = new UserAccount("usr_1", "alice", "alice@example.com", null); + user.setStatus(UserStatus.MERGED); + + given(credentialRepository.findByUsernameIgnoreCase("alice")).willReturn(Optional.of(credential)); + given(userAccountRepository.findById("usr_1")).willReturn(Optional.of(user)); + + assertThatThrownBy(() -> service.login("alice", "Abcd123!")) + .isInstanceOf(AuthFlowException.class) + .hasMessageContaining("error.auth.local.accountMerged"); + } + + @Test + void login_withoutExplicitRoles_defaultsToUser() { + LocalCredential credential = new LocalCredential("usr_1", "alice", "encoded"); + UserAccount user = new UserAccount("usr_1", "alice", "alice@example.com", null); + + given(credentialRepository.findByUsernameIgnoreCase("alice")).willReturn(Optional.of(credential)); + given(userAccountRepository.findById("usr_1")).willReturn(Optional.of(user)); + given(passwordEncoder.matches("Abcd123!", "encoded")).willReturn(true); + given(userRoleBindingRepository.findByUserId("usr_1")).willReturn(List.of()); + + var principal = service.login("alice", "Abcd123!"); + + assertThat(principal.platformRoles()).containsExactly("USER"); + } + @Test void register_rejectsInvalidEmailFormat() { given(credentialRepository.existsByUsernameIgnoreCase("alice")).willReturn(false); diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/namespace/NamespaceAccessPolicyTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/namespace/NamespaceAccessPolicyTest.java new file mode 100644 index 00000000..6e5e9f69 --- /dev/null +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/namespace/NamespaceAccessPolicyTest.java @@ -0,0 +1,57 @@ +package com.iflytek.skillhub.domain.namespace; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class NamespaceAccessPolicyTest { + + private final NamespaceAccessPolicy policy = new NamespaceAccessPolicy(); + + @Test + void globalNamespaceIsImmutable() { + Namespace namespace = new Namespace("global", "Global", "owner"); + namespace.setType(NamespaceType.GLOBAL); + + assertThat(policy.isImmutable(namespace)).isTrue(); + assertThat(policy.canMutateSettings(namespace)).isFalse(); + assertThat(policy.canManageMembers(namespace)).isFalse(); + assertThat(policy.canTransferOwnership(namespace)).isFalse(); + } + + @Test + void activeTeamNamespaceAllowsAdminAndOwnerToFreezeButNotMember() { + Namespace namespace = new Namespace("team-a", "Team A", "owner"); + namespace.setType(NamespaceType.TEAM); + namespace.setStatus(NamespaceStatus.ACTIVE); + + assertThat(policy.canFreeze(namespace, NamespaceRole.OWNER)).isTrue(); + assertThat(policy.canFreeze(namespace, NamespaceRole.ADMIN)).isTrue(); + assertThat(policy.canFreeze(namespace, NamespaceRole.MEMBER)).isFalse(); + } + + @Test + void frozenTeamNamespaceAllowsAdminAndOwnerToUnfreezeButNotMember() { + Namespace namespace = new Namespace("team-a", "Team A", "owner"); + namespace.setType(NamespaceType.TEAM); + namespace.setStatus(NamespaceStatus.FROZEN); + + assertThat(policy.canUnfreeze(namespace, NamespaceRole.OWNER)).isTrue(); + assertThat(policy.canUnfreeze(namespace, NamespaceRole.ADMIN)).isTrue(); + assertThat(policy.canUnfreeze(namespace, NamespaceRole.MEMBER)).isFalse(); + } + + @Test + void archiveAndRestoreAreOwnerOnly() { + Namespace namespace = new Namespace("team-a", "Team A", "owner"); + namespace.setType(NamespaceType.TEAM); + namespace.setStatus(NamespaceStatus.ACTIVE); + + assertThat(policy.canArchive(namespace, NamespaceRole.OWNER)).isTrue(); + assertThat(policy.canArchive(namespace, NamespaceRole.ADMIN)).isFalse(); + + namespace.setStatus(NamespaceStatus.ARCHIVED); + assertThat(policy.canRestore(namespace, NamespaceRole.OWNER)).isTrue(); + assertThat(policy.canRestore(namespace, NamespaceRole.ADMIN)).isFalse(); + } +}