Merge remote-tracking branch 'origin/main' into feature/project-fixbug

This commit is contained in:
yun-zhi-ztl 2026-03-17 14:31:30 +08:00
commit a4c0ea60a5
5 changed files with 218 additions and 0 deletions

View file

@ -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));
}
}

View file

@ -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")

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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();
}
}