From dd4f8d8abe95aeca23904d3c3681466b13ea9d2d Mon Sep 17 00:00:00 2001 From: vsxd Date: Thu, 12 Mar 2026 18:06:03 +0800 Subject: [PATCH] feat(social): add SkillStar and SkillRating controllers --- .../portal/SkillRatingController.java | 48 +++++++ .../portal/SkillStarController.java | 47 +++++++ .../controller/SkillRatingControllerTest.java | 107 +++++++++++++++ .../controller/SkillStarControllerTest.java | 123 ++++++++++++++++++ 4 files changed, 325 insertions(+) create mode 100644 server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillRatingController.java create mode 100644 server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillStarController.java create mode 100644 server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillRatingControllerTest.java create mode 100644 server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillStarControllerTest.java diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillRatingController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillRatingController.java new file mode 100644 index 00000000..bf017cde --- /dev/null +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillRatingController.java @@ -0,0 +1,48 @@ +package com.iflytek.skillhub.controller.portal; + +import com.iflytek.skillhub.auth.rbac.PlatformPrincipal; +import com.iflytek.skillhub.controller.BaseApiController; +import com.iflytek.skillhub.dto.ApiResponse; +import com.iflytek.skillhub.dto.ApiResponseFactory; +import com.iflytek.skillhub.domain.social.SkillRatingService; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; +import java.util.Optional; + +@RestController +@RequestMapping("/api/v1/skills") +public class SkillRatingController extends BaseApiController { + + private final SkillRatingService skillRatingService; + + public SkillRatingController(ApiResponseFactory responseFactory, + SkillRatingService skillRatingService) { + super(responseFactory); + this.skillRatingService = skillRatingService; + } + + @PutMapping("/{skillId}/rating") + public ResponseEntity rateSkill( + @PathVariable Long skillId, + @RequestBody Map request, + @AuthenticationPrincipal PlatformPrincipal principal) { + Short score = request.get("score"); + skillRatingService.rate(skillId, principal.userId(), score); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/{skillId}/rating") + public ApiResponse> getUserRating( + @PathVariable Long skillId, + @AuthenticationPrincipal PlatformPrincipal principal) { + Optional rating = skillRatingService.getUserRating(skillId, principal.userId()); + Map data = Map.of( + "score", rating.orElse((short) 0), + "rated", rating.isPresent() + ); + return ok("response.success.skill.rating.get", data); + } +} diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillStarController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillStarController.java new file mode 100644 index 00000000..f937e2ca --- /dev/null +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillStarController.java @@ -0,0 +1,47 @@ +package com.iflytek.skillhub.controller.portal; + +import com.iflytek.skillhub.auth.rbac.PlatformPrincipal; +import com.iflytek.skillhub.controller.BaseApiController; +import com.iflytek.skillhub.dto.ApiResponse; +import com.iflytek.skillhub.dto.ApiResponseFactory; +import com.iflytek.skillhub.domain.social.SkillStarService; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/skills") +public class SkillStarController extends BaseApiController { + + private final SkillStarService skillStarService; + + public SkillStarController(ApiResponseFactory responseFactory, + SkillStarService skillStarService) { + super(responseFactory); + this.skillStarService = skillStarService; + } + + @PutMapping("/{skillId}/star") + public ResponseEntity starSkill( + @PathVariable Long skillId, + @AuthenticationPrincipal PlatformPrincipal principal) { + skillStarService.star(skillId, principal.userId()); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/{skillId}/star") + public ResponseEntity unstarSkill( + @PathVariable Long skillId, + @AuthenticationPrincipal PlatformPrincipal principal) { + skillStarService.unstar(skillId, principal.userId()); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/{skillId}/star") + public ApiResponse checkStarred( + @PathVariable Long skillId, + @AuthenticationPrincipal PlatformPrincipal principal) { + boolean starred = skillStarService.isStarred(skillId, principal.userId()); + return ok("response.success.skill.star.check", starred); + } +} diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillRatingControllerTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillRatingControllerTest.java new file mode 100644 index 00000000..840906ae --- /dev/null +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillRatingControllerTest.java @@ -0,0 +1,107 @@ +package com.iflytek.skillhub.controller; + +import com.iflytek.skillhub.auth.rbac.PlatformPrincipal; +import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository; +import com.iflytek.skillhub.domain.social.SkillRatingService; +import org.junit.jupiter.api.Test; +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.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 java.util.List; +import java.util.Optional; +import java.util.Set; + +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; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +class SkillRatingControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private SkillRatingService skillRatingService; + + @MockBean + private NamespaceMemberRepository namespaceMemberRepository; + + @Test + void rate_skill_returns_204() throws Exception { + PlatformPrincipal principal = new PlatformPrincipal( + "user-42", + "tester", + "tester@example.com", + "https://example.com/avatar.png", + "github", + Set.of("SUPER_ADMIN") + ); + var auth = new UsernamePasswordAuthenticationToken( + principal, + null, + List.of(new SimpleGrantedAuthority("ROLE_SUPER_ADMIN")) + ); + + mockMvc.perform(put("/api/v1/skills/10/rating") + .with(authentication(auth)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"score\": 4}")) + .andExpect(status().isNoContent()); + + verify(skillRatingService).rate(eq(10L), eq("user-42"), eq((short) 4)); + } + + @Test + void get_user_rating_returns_score() throws Exception { + PlatformPrincipal principal = new PlatformPrincipal( + "user-42", + "tester", + "tester@example.com", + "https://example.com/avatar.png", + "github", + Set.of("SUPER_ADMIN") + ); + var auth = new UsernamePasswordAuthenticationToken( + principal, + null, + List.of(new SimpleGrantedAuthority("ROLE_SUPER_ADMIN")) + ); + + when(skillRatingService.getUserRating(eq(10L), eq("user-42"))) + .thenReturn(Optional.of((short) 4)); + + mockMvc.perform(get("/api/v1/skills/10/rating") + .with(authentication(auth)) + .with(csrf())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data.score").value(4)) + .andExpect(jsonPath("$.data.rated").value(true)) + .andExpect(jsonPath("$.timestamp").isNotEmpty()) + .andExpect(jsonPath("$.requestId").isNotEmpty()); + } + + @Test + void rate_skill_unauthenticated_returns_401() throws Exception { + mockMvc.perform(put("/api/v1/skills/10/rating") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content("{\"score\": 4}")) + .andExpect(status().isUnauthorized()); + } +} diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillStarControllerTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillStarControllerTest.java new file mode 100644 index 00000000..d3b6734b --- /dev/null +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/SkillStarControllerTest.java @@ -0,0 +1,123 @@ +package com.iflytek.skillhub.controller; + +import com.iflytek.skillhub.auth.rbac.PlatformPrincipal; +import com.iflytek.skillhub.domain.namespace.NamespaceMemberRepository; +import com.iflytek.skillhub.domain.social.SkillStarService; +import org.junit.jupiter.api.Test; +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.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.List; +import java.util.Set; + +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; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +@ActiveProfiles("test") +class SkillStarControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private SkillStarService skillStarService; + + @MockBean + private NamespaceMemberRepository namespaceMemberRepository; + + @Test + void star_skill_returns_204() throws Exception { + PlatformPrincipal principal = new PlatformPrincipal( + "user-42", + "tester", + "tester@example.com", + "https://example.com/avatar.png", + "github", + Set.of("SUPER_ADMIN") + ); + var auth = new UsernamePasswordAuthenticationToken( + principal, + null, + List.of(new SimpleGrantedAuthority("ROLE_SUPER_ADMIN")) + ); + + mockMvc.perform(put("/api/v1/skills/10/star") + .with(authentication(auth)) + .with(csrf())) + .andExpect(status().isNoContent()); + + verify(skillStarService).star(eq(10L), eq("user-42")); + } + + @Test + void unstar_skill_returns_204() throws Exception { + PlatformPrincipal principal = new PlatformPrincipal( + "user-42", + "tester", + "tester@example.com", + "https://example.com/avatar.png", + "github", + Set.of("SUPER_ADMIN") + ); + var auth = new UsernamePasswordAuthenticationToken( + principal, + null, + List.of(new SimpleGrantedAuthority("ROLE_SUPER_ADMIN")) + ); + + mockMvc.perform(delete("/api/v1/skills/10/star") + .with(authentication(auth)) + .with(csrf())) + .andExpect(status().isNoContent()); + + verify(skillStarService).unstar(eq(10L), eq("user-42")); + } + + @Test + void star_skill_unauthenticated_returns_401() throws Exception { + mockMvc.perform(put("/api/v1/skills/10/star") + .with(csrf())) + .andExpect(status().isUnauthorized()); + } + + @Test + void check_starred_returns_true() throws Exception { + PlatformPrincipal principal = new PlatformPrincipal( + "user-42", + "tester", + "tester@example.com", + "https://example.com/avatar.png", + "github", + Set.of("SUPER_ADMIN") + ); + var auth = new UsernamePasswordAuthenticationToken( + principal, + null, + List.of(new SimpleGrantedAuthority("ROLE_SUPER_ADMIN")) + ); + + when(skillStarService.isStarred(eq(10L), eq("user-42"))).thenReturn(true); + + mockMvc.perform(get("/api/v1/skills/10/star") + .with(authentication(auth)) + .with(csrf())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(0)) + .andExpect(jsonPath("$.data").value(true)) + .andExpect(jsonPath("$.timestamp").isNotEmpty()) + .andExpect(jsonPath("$.requestId").isNotEmpty()); + } +}