Browse Source

feat: add company onboarding knowledge APIs

backup/master-local-20261012
pwklppg 1 month ago
parent
commit
a5730a7acb
  1. 3
      mydreamjob-server/src/main/java/com/mydreamjob/common/exception/ErrorCode.java
  2. 3
      mydreamjob-server/src/main/java/com/mydreamjob/common/exception/GlobalExceptionHandler.java
  3. 74
      mydreamjob-server/src/main/java/com/mydreamjob/onboarding/CompanyKnowledgeController.java
  4. 386
      mydreamjob-server/src/main/java/com/mydreamjob/onboarding/CompanyKnowledgeService.java
  5. 53
      mydreamjob-server/src/main/java/com/mydreamjob/onboarding/dto/CompanyKnowledgeOverviewResponse.java
  6. 21
      mydreamjob-server/src/main/java/com/mydreamjob/onboarding/dto/CompanyKnowledgeUpdateRequest.java
  7. 24
      mydreamjob-server/src/main/java/com/mydreamjob/onboarding/dto/CompanyKnowledgeView.java
  8. 37
      mydreamjob-server/src/main/java/com/mydreamjob/onboarding/dto/ReviewSummaryView.java
  9. 228
      mydreamjob-server/src/test/java/com/mydreamjob/onboarding/CompanyKnowledgeControllerTest.java
  10. 376
      mydreamjob-server/src/test/java/com/mydreamjob/onboarding/CompanyKnowledgeServiceTest.java

3
mydreamjob-server/src/main/java/com/mydreamjob/common/exception/ErrorCode.java

@ -177,7 +177,8 @@ public enum ErrorCode {
ONBOARDING_CHECKLIST_ITEM_CONCURRENT_UPDATE(9702, "onboarding checklist item was modified concurrently"), ONBOARDING_CHECKLIST_ITEM_CONCURRENT_UPDATE(9702, "onboarding checklist item was modified concurrently"),
ONBOARDING_ITEM_LIMIT_EXCEEDED(9703, "onboarding checklist item limit exceeded"), ONBOARDING_ITEM_LIMIT_EXCEEDED(9703, "onboarding checklist item limit exceeded"),
ONBOARDING_IDEMPOTENCY_CONFLICT(9704, "idempotency key conflict: request body differs"), ONBOARDING_IDEMPOTENCY_CONFLICT(9704, "idempotency key conflict: request body differs"),
ONBOARDING_PROGRESS_CONCURRENT_UPDATE(9705, "onboarding progress was modified concurrently"); ONBOARDING_PROGRESS_CONCURRENT_UPDATE(9705, "onboarding progress was modified concurrently"),
ONBOARDING_KNOWLEDGE_CONCURRENT_UPDATE(9706, "company knowledge was modified concurrently");
private final int code; private final int code;
private final String message; private final String message;

3
mydreamjob-server/src/main/java/com/mydreamjob/common/exception/GlobalExceptionHandler.java

@ -168,7 +168,8 @@ public class GlobalExceptionHandler {
if (errorCode == ErrorCode.ONBOARDING_CHECKLIST_ITEM_CONCURRENT_UPDATE.getCode() if (errorCode == ErrorCode.ONBOARDING_CHECKLIST_ITEM_CONCURRENT_UPDATE.getCode()
|| errorCode == ErrorCode.ONBOARDING_ITEM_LIMIT_EXCEEDED.getCode() || errorCode == ErrorCode.ONBOARDING_ITEM_LIMIT_EXCEEDED.getCode()
|| errorCode == ErrorCode.ONBOARDING_IDEMPOTENCY_CONFLICT.getCode() || errorCode == ErrorCode.ONBOARDING_IDEMPOTENCY_CONFLICT.getCode()
|| errorCode == ErrorCode.ONBOARDING_PROGRESS_CONCURRENT_UPDATE.getCode()) || errorCode == ErrorCode.ONBOARDING_PROGRESS_CONCURRENT_UPDATE.getCode()
|| errorCode == ErrorCode.ONBOARDING_KNOWLEDGE_CONCURRENT_UPDATE.getCode())
return HttpStatus.CONFLICT; return HttpStatus.CONFLICT;
// 举报限流 → 429 // 举报限流 → 429
if (errorCode == ErrorCode.REPORT_RATE_LIMITED.getCode()) if (errorCode == ErrorCode.REPORT_RATE_LIMITED.getCode())

74
mydreamjob-server/src/main/java/com/mydreamjob/onboarding/CompanyKnowledgeController.java

@ -0,0 +1,74 @@
package com.mydreamjob.onboarding;
import com.mydreamjob.common.api.ApiResponse;
import com.mydreamjob.common.exception.BusinessException;
import com.mydreamjob.common.exception.ErrorCode;
import com.mydreamjob.onboarding.dto.CompanyKnowledgeOverviewResponse;
import com.mydreamjob.onboarding.dto.CompanyKnowledgeUpdateRequest;
import com.mydreamjob.onboarding.dto.CompanyKnowledgeView;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "公司知识介绍", description = "企业维护知识内容,学生读取公司知识聚合")
@RestController
@RequestMapping("/onboarding")
@RequiredArgsConstructor
public class CompanyKnowledgeController {
private final CompanyKnowledgeService companyKnowledgeService;
@Operation(summary = "读取本企业知识配置")
@GetMapping("/company/knowledge")
public ResponseEntity<ApiResponse<CompanyKnowledgeView>> getCompanyKnowledge(
HttpServletRequest request) {
Long userId = resolveUserId();
CompanyKnowledgeView data = companyKnowledgeService.getCompanyKnowledge(userId);
return ResponseEntity.ok(ApiResponse.ok(data, getRequestId(request)));
}
@Operation(summary = "创建或更新本企业知识配置")
@PutMapping("/company/knowledge")
public ResponseEntity<ApiResponse<CompanyKnowledgeView>> upsertCompanyKnowledge(
@Valid @RequestBody CompanyKnowledgeUpdateRequest body,
HttpServletRequest request) {
Long userId = resolveUserId();
CompanyKnowledgeView data =
companyKnowledgeService.upsertCompanyKnowledge(userId, body);
return ResponseEntity.ok(ApiResponse.ok(data, getRequestId(request)));
}
@Operation(summary = "读取公司知识介绍")
@GetMapping("/companies/{companyId}/knowledge")
public ResponseEntity<ApiResponse<CompanyKnowledgeOverviewResponse>> getCompanyKnowledgeOverview(
@PathVariable Long companyId,
HttpServletRequest request) {
Long userId = resolveUserId();
CompanyKnowledgeOverviewResponse data =
companyKnowledgeService.getCompanyKnowledgeOverview(userId, companyId);
return ResponseEntity.ok(ApiResponse.ok(data, getRequestId(request)));
}
private Long resolveUserId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
throw new BusinessException(ErrorCode.UNAUTHORIZED);
}
return Long.valueOf(authentication.getName());
}
private String getRequestId(HttpServletRequest request) {
return request.getHeader("X-Request-Id");
}
}

386
mydreamjob-server/src/main/java/com/mydreamjob/onboarding/CompanyKnowledgeService.java

@ -0,0 +1,386 @@
package com.mydreamjob.onboarding;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mydreamjob.auth.User;
import com.mydreamjob.auth.UserRepository;
import com.mydreamjob.common.exception.BusinessException;
import com.mydreamjob.common.exception.ErrorCode;
import com.mydreamjob.company.Company;
import com.mydreamjob.company.CompanyRepository;
import com.mydreamjob.onboarding.dto.CompanyKnowledgeOverviewResponse;
import com.mydreamjob.onboarding.dto.CompanyKnowledgeUpdateRequest;
import com.mydreamjob.onboarding.dto.CompanyKnowledgeView;
import com.mydreamjob.onboarding.dto.ReviewSummaryView;
import com.mydreamjob.review.JobReviewDimensionScoreRepository;
import com.mydreamjob.review.JobReviewRepository;
import com.mydreamjob.review.ReviewDimension;
import lombok.RequiredArgsConstructor;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class CompanyKnowledgeService {
private static final String CONFIGURED = "CONFIGURED";
private static final String BASIC_ONLY = "BASIC_ONLY";
private static final String EMPTY = "EMPTY";
private static final String INSUFFICIENT = "INSUFFICIENT";
private static final String SUFFICIENT = "SUFFICIENT";
private static final int MINIMUM_PUBLIC_REVIEWS = 3;
private static final int MAX_TOP_TAGS = 10;
private static final int MAX_CULTURE_LENGTH = 2000;
private static final int MAX_WORK_POLICIES_LENGTH = 4000;
private static final int MAX_ONBOARDING_GUIDE_LENGTH = 4000;
private static final Pattern HTML_TAG_PATTERN =
Pattern.compile("<\\s*/?\\s*[A-Za-z!][^>]*>");
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private final UserRepository userRepository;
private final CompanyRepository companyRepository;
private final CompanyKnowledgeRepository companyKnowledgeRepository;
private final JobReviewRepository jobReviewRepository;
private final JobReviewDimensionScoreRepository dimensionScoreRepository;
@Transactional(readOnly = true)
public CompanyKnowledgeView getCompanyKnowledge(Long userId) {
Company company = requireOwnedCompany(userId);
return companyKnowledgeRepository.findByCompanyId(company.getId())
.map(this::toConfiguredView)
.orElseGet(() -> CompanyKnowledgeView.builder()
.configured(false)
.version(0L)
.build());
}
@Transactional
public CompanyKnowledgeView upsertCompanyKnowledge(
Long userId,
CompanyKnowledgeUpdateRequest request) {
Company company = requireOwnedCompany(userId);
KnowledgeContent content = validateContent(request);
CompanyKnowledge existing = companyKnowledgeRepository.findByCompanyId(company.getId())
.orElse(null);
if (existing == null) {
CompanyKnowledge created = CompanyKnowledge.builder()
.companyId(company.getId())
.culture(content.culture())
.workPolicies(content.workPolicies())
.onboardingGuide(content.onboardingGuide())
.build();
return toConfiguredView(saveKnowledge(created));
}
if (request.getVersion() == null) {
throw new BusinessException(ErrorCode.BAD_REQUEST);
}
if (!Objects.equals(request.getVersion(), existing.getVersion())) {
throw new BusinessException(ErrorCode.ONBOARDING_KNOWLEDGE_CONCURRENT_UPDATE);
}
existing.setCulture(content.culture());
existing.setWorkPolicies(content.workPolicies());
existing.setOnboardingGuide(content.onboardingGuide());
return toConfiguredView(saveKnowledge(existing));
}
@Transactional(readOnly = true)
public CompanyKnowledgeOverviewResponse getCompanyKnowledgeOverview(
Long userId,
Long companyId) {
assertStudentRole(userId);
Company company = companyRepository.findById(companyId)
.orElseThrow(() -> new BusinessException(ErrorCode.COMPANY_NOT_FOUND));
CompanyKnowledge knowledge = companyKnowledgeRepository.findByCompanyId(companyId)
.orElse(null);
return CompanyKnowledgeOverviewResponse.builder()
.company(toCompanyView(company))
.knowledgeState(knowledge == null ? BASIC_ONLY : CONFIGURED)
.knowledge(knowledge == null ? null : toKnowledgeView(knowledge))
.reviewSummary(buildReviewSummary(companyId))
.build();
}
private CompanyKnowledge saveKnowledge(CompanyKnowledge knowledge) {
try {
return companyKnowledgeRepository.saveAndFlush(knowledge);
} catch (ObjectOptimisticLockingFailureException ex) {
throw new BusinessException(
ErrorCode.ONBOARDING_KNOWLEDGE_CONCURRENT_UPDATE, ex);
}
}
private KnowledgeContent validateContent(CompanyKnowledgeUpdateRequest request) {
if (request == null) {
throw new BusinessException(ErrorCode.BAD_REQUEST);
}
if (request.getVersion() != null && request.getVersion() < 0) {
throw new BusinessException(ErrorCode.BAD_REQUEST);
}
String culture = normalizeText(request.getCulture(), MAX_CULTURE_LENGTH);
String workPolicies = normalizeText(
request.getWorkPolicies(), MAX_WORK_POLICIES_LENGTH);
String onboardingGuide = normalizeText(
request.getOnboardingGuide(), MAX_ONBOARDING_GUIDE_LENGTH);
if (culture == null && workPolicies == null && onboardingGuide == null) {
throw new BusinessException(ErrorCode.BAD_REQUEST);
}
return new KnowledgeContent(culture, workPolicies, onboardingGuide);
}
private String normalizeText(String raw, int maxLength) {
if (!StringUtils.hasText(raw)) {
return null;
}
String value = raw.trim();
if (value.length() > maxLength) {
throw new BusinessException(ErrorCode.BAD_REQUEST);
}
if (HTML_TAG_PATTERN.matcher(value).find()) {
throw new BusinessException(ErrorCode.BAD_REQUEST);
}
return value;
}
private ReviewSummaryView buildReviewSummary(Long companyId) {
List<Object[]> countRows =
jobReviewRepository.countAndAveragePublishedRatingByCompanyId(companyId);
long totalReviews = extractCount(countRows);
if (totalReviews == 0) {
return ReviewSummaryView.builder()
.state(EMPTY)
.totalReviews(0)
.build();
}
if (totalReviews < MINIMUM_PUBLIC_REVIEWS) {
return ReviewSummaryView.builder()
.state(INSUFFICIENT)
.totalReviews(Math.toIntExact(totalReviews))
.build();
}
return ReviewSummaryView.builder()
.state(SUFFICIENT)
.totalReviews(Math.toIntExact(totalReviews))
.averageRating(extractAverageRating(countRows))
.ratingDistribution(buildRatingDistribution(companyId))
.topTags(buildTopTags(companyId))
.dimensionSummary(buildDimensionSummary(companyId))
.build();
}
private long extractCount(List<Object[]> rows) {
if (rows == null || rows.isEmpty() || rows.get(0) == null
|| rows.get(0).length == 0 || !(rows.get(0)[0] instanceof Number count)) {
return 0L;
}
return count.longValue();
}
private Double extractAverageRating(List<Object[]> rows) {
if (rows == null || rows.isEmpty() || rows.get(0) == null
|| rows.get(0).length < 2 || !(rows.get(0)[1] instanceof Number average)) {
return 0.0;
}
return BigDecimal.valueOf(average.doubleValue())
.setScale(1, RoundingMode.HALF_UP)
.doubleValue();
}
private Map<String, Integer> buildRatingDistribution(Long companyId) {
Map<String, Integer> distribution = new LinkedHashMap<>();
for (int rating = 1; rating <= 5; rating++) {
distribution.put("rating" + rating, 0);
}
List<Object[]> rows =
jobReviewRepository.findPublishedRatingDistributionByCompanyId(companyId);
if (rows == null) {
return distribution;
}
for (Object[] row : rows) {
if (row == null || row.length < 2
|| !(row[0] instanceof Number rating)
|| !(row[1] instanceof Number count)) {
continue;
}
int ratingValue = rating.intValue();
if (ratingValue >= 1 && ratingValue <= 5) {
distribution.put("rating" + ratingValue, count.intValue());
}
}
return distribution;
}
private List<String> buildTopTags(Long companyId) {
Map<String, Long> counts = new HashMap<>();
List<String> rawTags = jobReviewRepository.findPublishedTagsByCompanyId(companyId);
if (rawTags != null) {
for (String rawTag : rawTags) {
for (String tag : parseTags(rawTag)) {
counts.merge(tag, 1L, Long::sum);
}
}
}
return counts.entrySet().stream()
.sorted((left, right) -> {
int byCount = Long.compare(right.getValue(), left.getValue());
return byCount != 0 ? byCount : left.getKey().compareTo(right.getKey());
})
.limit(MAX_TOP_TAGS)
.map(Map.Entry::getKey)
.toList();
}
private List<String> parseTags(String raw) {
if (!StringUtils.hasText(raw)) {
return List.of();
}
Object parsed = parseJson(raw);
for (int depth = 0; depth < 2 && parsed instanceof String nested; depth++) {
parsed = parseJson(nested);
}
if (!(parsed instanceof Collection<?> values)) {
return List.of();
}
List<String> tags = new ArrayList<>();
for (Object value : values) {
if (value instanceof String tag && StringUtils.hasText(tag)) {
tags.add(tag.trim());
}
}
return tags;
}
private Object parseJson(String raw) {
try {
return OBJECT_MAPPER.readValue(raw, Object.class);
} catch (JsonProcessingException ex) {
return null;
}
}
private Map<String, ReviewSummaryView.DimensionSummaryView> buildDimensionSummary(
Long companyId) {
Map<String, Object[]> rowsByCode = new HashMap<>();
List<Object[]> rows =
dimensionScoreRepository.aggregatePublishedDimensionsByCompanyId(companyId);
if (rows != null) {
for (Object[] row : rows) {
if (row != null && row.length >= 3 && row[0] instanceof String code) {
rowsByCode.put(code, row);
}
}
}
Map<String, ReviewSummaryView.DimensionSummaryView> result = new LinkedHashMap<>();
for (ReviewDimension dimension : ReviewDimension.values()) {
Object[] row = rowsByCode.get(dimension.name());
long sampleCount = 0L;
Double averageScore = null;
if (row != null && row[2] instanceof Number count) {
sampleCount = count.longValue();
}
if (row != null && row[1] instanceof Number average) {
averageScore = BigDecimal.valueOf(average.doubleValue())
.setScale(1, RoundingMode.HALF_UP)
.doubleValue();
}
result.put(dimension.name(), ReviewSummaryView.DimensionSummaryView.builder()
.averageScore(averageScore)
.sampleCount(sampleCount)
.dataState(sampleCount >= MINIMUM_PUBLIC_REVIEWS
? SUFFICIENT : INSUFFICIENT)
.build());
}
return result;
}
private CompanyKnowledgeView toConfiguredView(CompanyKnowledge knowledge) {
return CompanyKnowledgeView.builder()
.configured(true)
.culture(knowledge.getCulture())
.workPolicies(knowledge.getWorkPolicies())
.onboardingGuide(knowledge.getOnboardingGuide())
.updatedAt(knowledge.getUpdatedAt())
.version(knowledge.getVersion() == null ? 0L : knowledge.getVersion())
.build();
}
private CompanyKnowledgeOverviewResponse.KnowledgeView toKnowledgeView(
CompanyKnowledge knowledge) {
return CompanyKnowledgeOverviewResponse.KnowledgeView.builder()
.culture(knowledge.getCulture())
.workPolicies(knowledge.getWorkPolicies())
.onboardingGuide(knowledge.getOnboardingGuide())
.updatedAt(knowledge.getUpdatedAt())
.build();
}
private CompanyKnowledgeOverviewResponse.CompanyView toCompanyView(Company company) {
return CompanyKnowledgeOverviewResponse.CompanyView.builder()
.id(company.getId())
.name(company.getName())
.logoUrl(company.getLogoUrl())
.description(company.getDescription())
.industry(company.getIndustry())
.website(company.getWebsite())
.address(company.getAddress())
.verified(company.getVerified())
.build();
}
private Company requireOwnedCompany(Long userId) {
assertCompanyRole(userId);
return companyRepository.findByOwnerUserId(userId)
.orElseThrow(() -> new BusinessException(ErrorCode.COMPANY_NOT_FOUND));
}
private void assertCompanyRole(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
if (user.getRoles() == null
|| user.getRoles().stream().noneMatch(role -> role == User.Role.COMPANY)) {
throw new BusinessException(ErrorCode.FORBIDDEN);
}
}
private void assertStudentRole(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
if (user.getRoles() == null
|| user.getRoles().stream().noneMatch(role -> role == User.Role.STUDENT)) {
throw new BusinessException(ErrorCode.FORBIDDEN);
}
}
private record KnowledgeContent(
String culture,
String workPolicies,
String onboardingGuide) {
}
}

53
mydreamjob-server/src/main/java/com/mydreamjob/onboarding/dto/CompanyKnowledgeOverviewResponse.java

@ -0,0 +1,53 @@
package com.mydreamjob.onboarding.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class CompanyKnowledgeOverviewResponse {
private CompanyView company;
private String knowledgeState;
private KnowledgeView knowledge;
private ReviewSummaryView reviewSummary;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class CompanyView {
private Long id;
private String name;
private String logoUrl;
private String description;
private String industry;
private String website;
private String address;
private Boolean verified;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class KnowledgeView {
private String culture;
private String workPolicies;
private String onboardingGuide;
private LocalDateTime updatedAt;
private Long version;
}
}

21
mydreamjob-server/src/main/java/com/mydreamjob/onboarding/dto/CompanyKnowledgeUpdateRequest.java

@ -0,0 +1,21 @@
package com.mydreamjob.onboarding.dto;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Size;
import lombok.Data;
@Data
public class CompanyKnowledgeUpdateRequest {
@Size(max = 2000, message = "culture must be at most 2000 chars")
private String culture;
@Size(max = 4000, message = "workPolicies must be at most 4000 chars")
private String workPolicies;
@Size(max = 4000, message = "onboardingGuide must be at most 4000 chars")
private String onboardingGuide;
@Min(value = 0, message = "version must not be negative")
private Long version;
}

24
mydreamjob-server/src/main/java/com/mydreamjob/onboarding/dto/CompanyKnowledgeView.java

@ -0,0 +1,24 @@
package com.mydreamjob.onboarding.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class CompanyKnowledgeView {
private Boolean configured;
private String culture;
private String workPolicies;
private String onboardingGuide;
private LocalDateTime updatedAt;
private Long version;
}

37
mydreamjob-server/src/main/java/com/mydreamjob/onboarding/dto/ReviewSummaryView.java

@ -0,0 +1,37 @@
package com.mydreamjob.onboarding.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ReviewSummaryView {
private String state;
private int totalReviews;
private Double averageRating;
private Map<String, Integer> ratingDistribution;
private List<String> topTags;
private Map<String, DimensionSummaryView> dimensionSummary;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class DimensionSummaryView {
private Double averageScore;
private long sampleCount;
private String dataState;
}
}

228
mydreamjob-server/src/test/java/com/mydreamjob/onboarding/CompanyKnowledgeControllerTest.java

@ -0,0 +1,228 @@
package com.mydreamjob.onboarding;
import com.mydreamjob.auth.JwtTokenProvider;
import com.mydreamjob.common.exception.BusinessException;
import com.mydreamjob.common.exception.ErrorCode;
import com.mydreamjob.onboarding.dto.CompanyKnowledgeOverviewResponse;
import com.mydreamjob.onboarding.dto.CompanyKnowledgeUpdateRequest;
import com.mydreamjob.onboarding.dto.CompanyKnowledgeView;
import com.mydreamjob.onboarding.dto.ReviewSummaryView;
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.data.redis.core.RedisTemplate;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import java.time.LocalDateTime;
import java.util.Map;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("integration-test")
class CompanyKnowledgeControllerTest {
private static final Long COMPANY_USER_ID = 900L;
private static final Long STUDENT_USER_ID = 901L;
private static final Long COMPANY_ID = 7L;
@Autowired private MockMvc mockMvc;
@Autowired private JwtTokenProvider jwtTokenProvider;
@MockBean private CompanyKnowledgeService companyKnowledgeService;
@MockBean private RedisTemplate<String, Object> redisTemplate;
@Test
void getCompanyKnowledge_shouldReturnUnconfiguredContract() throws Exception {
when(companyKnowledgeService.getCompanyKnowledge(COMPANY_USER_ID))
.thenReturn(view(false, null, null, null, null, 0L));
mockMvc.perform(get("/onboarding/company/knowledge")
.header("Authorization", companyToken())
.header("X-Request-Id", "req-t5-get"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.requestId").value("req-t5-get"))
.andExpect(jsonPath("$.data.configured").value(false))
.andExpect(jsonPath("$.data.version").value(0))
.andExpect(jsonPath("$.data.culture").doesNotExist());
verify(companyKnowledgeService).getCompanyKnowledge(COMPANY_USER_ID);
}
@Test
void putCompanyKnowledge_shouldReturnConfiguredContract() throws Exception {
when(companyKnowledgeService.upsertCompanyKnowledge(
eq(COMPANY_USER_ID), any(CompanyKnowledgeUpdateRequest.class)))
.thenReturn(view(true, "Culture", "Policies", "Guide",
LocalDateTime.of(2026, 9, 16, 7, 0), 0L));
mockMvc.perform(put("/onboarding/company/knowledge")
.header("Authorization", companyToken())
.header("X-Request-Id", "req-t5-put")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"culture": "Culture",
"workPolicies": "Policies",
"onboardingGuide": "Guide"
}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.requestId").value("req-t5-put"))
.andExpect(jsonPath("$.data.configured").value(true))
.andExpect(jsonPath("$.data.culture").value("Culture"))
.andExpect(jsonPath("$.data.version").value(0));
verify(companyKnowledgeService).upsertCompanyKnowledge(
eq(COMPANY_USER_ID), argThat(request ->
"Culture".equals(request.getCulture())
&& "Policies".equals(request.getWorkPolicies())
&& "Guide".equals(request.getOnboardingGuide())
&& request.getVersion() == null));
}
@Test
void putCompanyKnowledge_withInvalidLength_shouldReturn400BeforeService() throws Exception {
String tooLong = "x".repeat(2001);
mockMvc.perform(put("/onboarding/company/knowledge")
.header("Authorization", companyToken())
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"culture":"%s"}
""".formatted(tooLong)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value(400));
verify(companyKnowledgeService, never()).upsertCompanyKnowledge(any(), any());
}
@Test
void putCompanyKnowledge_staleVersion_shouldReturn409() throws Exception {
when(companyKnowledgeService.upsertCompanyKnowledge(
eq(COMPANY_USER_ID), any(CompanyKnowledgeUpdateRequest.class)))
.thenThrow(new BusinessException(
ErrorCode.ONBOARDING_KNOWLEDGE_CONCURRENT_UPDATE));
mockMvc.perform(put("/onboarding/company/knowledge")
.header("Authorization", companyToken())
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"culture":"New","version":1}
"""))
.andExpect(status().isConflict())
.andExpect(jsonPath("$.code")
.value(ErrorCode.ONBOARDING_KNOWLEDGE_CONCURRENT_UPDATE.getCode()));
}
@Test
void getCompanyKnowledge_asStudent_shouldReturn403() throws Exception {
when(companyKnowledgeService.getCompanyKnowledge(STUDENT_USER_ID))
.thenThrow(new BusinessException(ErrorCode.FORBIDDEN));
mockMvc.perform(get("/onboarding/company/knowledge")
.header("Authorization", studentToken()))
.andExpect(status().isForbidden())
.andExpect(jsonPath("$.code").value(ErrorCode.FORBIDDEN.getCode()));
}
@Test
void studentOverview_shouldReturnWhitelistedCompanyAndReviewSummary() throws Exception {
when(companyKnowledgeService.getCompanyKnowledgeOverview(STUDENT_USER_ID, COMPANY_ID))
.thenReturn(overview());
mockMvc.perform(get("/onboarding/companies/{companyId}/knowledge", COMPANY_ID)
.header("Authorization", studentToken())
.header("X-Request-Id", "req-t5-overview"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.requestId").value("req-t5-overview"))
.andExpect(jsonPath("$.data.knowledgeState").value("BASIC_ONLY"))
.andExpect(jsonPath("$.data.company.id").value(COMPANY_ID))
.andExpect(jsonPath("$.data.company.name").value("Example Corp"))
.andExpect(jsonPath("$.data.company.website").value("https://example.com"))
.andExpect(jsonPath("$.data.company.address").value("Shanghai"))
.andExpect(jsonPath("$.data.company.ownerUserId").doesNotExist())
.andExpect(jsonPath("$.data.reviewSummary.state").value("EMPTY"))
.andExpect(jsonPath("$.data.reviewSummary.totalReviews").value(0));
verify(companyKnowledgeService).getCompanyKnowledgeOverview(STUDENT_USER_ID, COMPANY_ID);
}
@Test
void studentOverview_companyNotFound_shouldReturn404() throws Exception {
when(companyKnowledgeService.getCompanyKnowledgeOverview(STUDENT_USER_ID, COMPANY_ID))
.thenThrow(new BusinessException(ErrorCode.COMPANY_NOT_FOUND));
mockMvc.perform(get("/onboarding/companies/{companyId}/knowledge", COMPANY_ID)
.header("Authorization", studentToken()))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.code").value(ErrorCode.COMPANY_NOT_FOUND.getCode()));
}
@Test
void getCompanyKnowledge_withoutToken_shouldReturn401() throws Exception {
mockMvc.perform(get("/onboarding/company/knowledge"))
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.code").value(401));
}
private CompanyKnowledgeView view(
boolean configured,
String culture,
String workPolicies,
String onboardingGuide,
LocalDateTime updatedAt,
Long version) {
return CompanyKnowledgeView.builder()
.configured(configured)
.culture(culture)
.workPolicies(workPolicies)
.onboardingGuide(onboardingGuide)
.updatedAt(updatedAt)
.version(version)
.build();
}
private CompanyKnowledgeOverviewResponse overview() {
return CompanyKnowledgeOverviewResponse.builder()
.company(CompanyKnowledgeOverviewResponse.CompanyView.builder()
.id(COMPANY_ID)
.name("Example Corp")
.logoUrl("https://example.com/logo.png")
.description("Description")
.industry("Software")
.website("https://example.com")
.address("Shanghai")
.verified(true)
.build())
.knowledgeState("BASIC_ONLY")
.reviewSummary(ReviewSummaryView.builder()
.state("EMPTY")
.totalReviews(0)
.build())
.build();
}
private String companyToken() {
return "Bearer " + jwtTokenProvider.generateAccessToken(
COMPANY_USER_ID, "1", Map.of("roles", "COMPANY"));
}
private String studentToken() {
return "Bearer " + jwtTokenProvider.generateAccessToken(
STUDENT_USER_ID, "1", Map.of("roles", "STUDENT"));
}
}

376
mydreamjob-server/src/test/java/com/mydreamjob/onboarding/CompanyKnowledgeServiceTest.java

@ -0,0 +1,376 @@
package com.mydreamjob.onboarding;
import com.mydreamjob.auth.User;
import com.mydreamjob.auth.UserRepository;
import com.mydreamjob.common.exception.BusinessException;
import com.mydreamjob.common.exception.ErrorCode;
import com.mydreamjob.company.Company;
import com.mydreamjob.company.CompanyRepository;
import com.mydreamjob.onboarding.dto.CompanyKnowledgeOverviewResponse;
import com.mydreamjob.onboarding.dto.CompanyKnowledgeUpdateRequest;
import com.mydreamjob.onboarding.dto.CompanyKnowledgeView;
import com.mydreamjob.onboarding.dto.ReviewSummaryView;
import com.mydreamjob.review.JobReviewDimensionScoreRepository;
import com.mydreamjob.review.JobReviewRepository;
import com.mydreamjob.review.ReviewDimension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.Set;
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.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CompanyKnowledgeServiceTest {
private static final Long COMPANY_USER_ID = 900L;
private static final Long STUDENT_USER_ID = 901L;
private static final Long COMPANY_ID = 7L;
private static final LocalDateTime UPDATED_AT = LocalDateTime.of(2026, 9, 16, 7, 0);
@Mock private UserRepository userRepository;
@Mock private CompanyRepository companyRepository;
@Mock private CompanyKnowledgeRepository knowledgeRepository;
@Mock private JobReviewRepository reviewRepository;
@Mock private JobReviewDimensionScoreRepository dimensionScoreRepository;
@InjectMocks private CompanyKnowledgeService service;
@Test
void getCompanyKnowledge_withoutRecord_shouldReturnUnconfiguredEmptyView() {
stubCompanyOwner();
when(knowledgeRepository.findByCompanyId(COMPANY_ID)).thenReturn(Optional.empty());
CompanyKnowledgeView result = service.getCompanyKnowledge(COMPANY_USER_ID);
assertThat(result.getConfigured()).isFalse();
assertThat(result.getCulture()).isNull();
assertThat(result.getWorkPolicies()).isNull();
assertThat(result.getOnboardingGuide()).isNull();
assertThat(result.getUpdatedAt()).isNull();
assertThat(result.getVersion()).isZero();
}
@Test
void getCompanyKnowledge_withRecord_shouldReturnConfiguredView() {
stubCompanyOwner();
when(knowledgeRepository.findByCompanyId(COMPANY_ID)).thenReturn(Optional.of(
knowledge(1L, "Culture", "Policies", "Guide", 3L, UPDATED_AT)));
CompanyKnowledgeView result = service.getCompanyKnowledge(COMPANY_USER_ID);
assertThat(result.getConfigured()).isTrue();
assertThat(result.getCulture()).isEqualTo("Culture");
assertThat(result.getWorkPolicies()).isEqualTo("Policies");
assertThat(result.getOnboardingGuide()).isEqualTo("Guide");
assertThat(result.getUpdatedAt()).isEqualTo(UPDATED_AT);
assertThat(result.getVersion()).isEqualTo(3L);
}
@Test
void upsertCompanyKnowledge_firstCreate_shouldTrimAndPersist() {
stubCompanyOwner();
when(knowledgeRepository.findByCompanyId(COMPANY_ID)).thenReturn(Optional.empty());
ArgumentCaptor<CompanyKnowledge> captor = ArgumentCaptor.forClass(CompanyKnowledge.class);
when(knowledgeRepository.saveAndFlush(any(CompanyKnowledge.class))).thenAnswer(invocation -> {
CompanyKnowledge saved = invocation.getArgument(0);
saved.setId(11L);
saved.setVersion(0L);
saved.setUpdatedAt(UPDATED_AT);
return saved;
});
CompanyKnowledgeView result = service.upsertCompanyKnowledge(
COMPANY_USER_ID,
request(" Culture ", " Policies ", " Guide ", null));
verify(knowledgeRepository).saveAndFlush(captor.capture());
CompanyKnowledge saved = captor.getValue();
assertThat(saved.getCompanyId()).isEqualTo(COMPANY_ID);
assertThat(saved.getCulture()).isEqualTo("Culture");
assertThat(saved.getWorkPolicies()).isEqualTo("Policies");
assertThat(saved.getOnboardingGuide()).isEqualTo("Guide");
assertThat(result.getConfigured()).isTrue();
assertThat(result.getVersion()).isZero();
}
@Test
void upsertCompanyKnowledge_updateWithCurrentVersion_shouldIncrementVersion() {
stubCompanyOwner();
CompanyKnowledge existing = knowledge(12L, "Old culture", null, null, 2L, UPDATED_AT);
when(knowledgeRepository.findByCompanyId(COMPANY_ID)).thenReturn(Optional.of(existing));
when(knowledgeRepository.saveAndFlush(existing)).thenAnswer(invocation -> {
existing.setVersion(3L);
existing.setUpdatedAt(UPDATED_AT.plusHours(1));
return existing;
});
CompanyKnowledgeView result = service.upsertCompanyKnowledge(
COMPANY_USER_ID, request("New culture", null, "New guide", 2L));
assertThat(existing.getCulture()).isEqualTo("New culture");
assertThat(existing.getWorkPolicies()).isNull();
assertThat(existing.getOnboardingGuide()).isEqualTo("New guide");
assertThat(result.getVersion()).isEqualTo(3L);
}
@Test
void upsertCompanyKnowledge_existingWithoutVersion_shouldReturnBadRequest() {
stubCompanyOwner();
when(knowledgeRepository.findByCompanyId(COMPANY_ID)).thenReturn(Optional.of(
knowledge(1L, "culture", null, null, 1L, UPDATED_AT)));
assertThatThrownBy(() -> service.upsertCompanyKnowledge(
COMPANY_USER_ID, request("new", null, null, null)))
.isInstanceOf(BusinessException.class)
.extracting(ex -> ((BusinessException) ex).getErrorCodeInt())
.isEqualTo(ErrorCode.BAD_REQUEST.getCode());
verify(knowledgeRepository, never()).saveAndFlush(any());
}
@Test
void upsertCompanyKnowledge_staleVersion_shouldReturnKnowledgeConflict() {
stubCompanyOwner();
when(knowledgeRepository.findByCompanyId(COMPANY_ID)).thenReturn(Optional.of(
knowledge(1L, "culture", null, null, 4L, UPDATED_AT)));
assertThatThrownBy(() -> service.upsertCompanyKnowledge(
COMPANY_USER_ID, request("new", null, null, 3L)))
.isInstanceOf(BusinessException.class)
.extracting(ex -> ((BusinessException) ex).getErrorCodeInt())
.isEqualTo(ErrorCode.ONBOARDING_KNOWLEDGE_CONCURRENT_UPDATE.getCode());
verify(knowledgeRepository, never()).saveAndFlush(any());
}
@Test
void upsertCompanyKnowledge_optimisticLockFailure_shouldReturnKnowledgeConflict() {
stubCompanyOwner();
CompanyKnowledge existing = knowledge(1L, "culture", null, null, 1L, UPDATED_AT);
when(knowledgeRepository.findByCompanyId(COMPANY_ID)).thenReturn(Optional.of(existing));
when(knowledgeRepository.saveAndFlush(existing))
.thenThrow(new ObjectOptimisticLockingFailureException(CompanyKnowledge.class, 1L));
assertThatThrownBy(() -> service.upsertCompanyKnowledge(
COMPANY_USER_ID, request("new", null, null, 1L)))
.isInstanceOf(BusinessException.class)
.extracting(ex -> ((BusinessException) ex).getErrorCodeInt())
.isEqualTo(ErrorCode.ONBOARDING_KNOWLEDGE_CONCURRENT_UPDATE.getCode());
}
@Test
void upsertCompanyKnowledge_invalidPlainTextFields_shouldReturnBadRequest() {
stubCompanyOwner();
List<CompanyKnowledgeUpdateRequest> invalid = List.of(
request(null, null, null, null),
request(" ", " ", " ", null),
request("x".repeat(2001), null, null, null),
request(null, "x".repeat(4001), null, null),
request(null, null, "x".repeat(4001), null),
request("<script>alert(1)</script>", null, null, null));
for (CompanyKnowledgeUpdateRequest value : invalid) {
assertThatThrownBy(() -> service.upsertCompanyKnowledge(COMPANY_USER_ID, value))
.isInstanceOf(BusinessException.class)
.extracting(ex -> ((BusinessException) ex).getErrorCodeInt())
.isEqualTo(ErrorCode.BAD_REQUEST.getCode());
}
verifyNoInteractions(knowledgeRepository);
}
@Test
void overview_withoutKnowledge_shouldReturnBasicOnlyAndEmptyReviewSummary() {
stubStudent();
when(companyRepository.findById(COMPANY_ID)).thenReturn(Optional.of(company()));
when(knowledgeRepository.findByCompanyId(COMPANY_ID)).thenReturn(Optional.empty());
when(reviewRepository.countAndAveragePublishedRatingByCompanyId(COMPANY_ID))
.thenReturn(List.<Object[]>of(new Object[]{0L, BigDecimal.ZERO}));
CompanyKnowledgeOverviewResponse result =
service.getCompanyKnowledgeOverview(STUDENT_USER_ID, COMPANY_ID);
assertThat(result.getKnowledgeState()).isEqualTo("BASIC_ONLY");
assertThat(result.getKnowledge()).isNull();
assertThat(result.getCompany().getId()).isEqualTo(COMPANY_ID);
assertThat(result.getCompany().getWebsite()).isEqualTo("https://example.com");
assertThat(result.getCompany().getAddress()).isEqualTo("Shanghai");
assertThat(result.getReviewSummary().getState()).isEqualTo("EMPTY");
assertThat(result.getReviewSummary().getTotalReviews()).isZero();
assertThat(result.getReviewSummary().getAverageRating()).isNull();
assertThat(result.getReviewSummary().getDimensionSummary()).isNull();
}
@Test
void overview_withKnowledge_shouldReturnConfiguredKnowledge() {
stubStudent();
when(companyRepository.findById(COMPANY_ID)).thenReturn(Optional.of(company()));
when(knowledgeRepository.findByCompanyId(COMPANY_ID)).thenReturn(Optional.of(
knowledge(5L, "Culture", "Policies", "Guide", 2L, UPDATED_AT)));
when(reviewRepository.countAndAveragePublishedRatingByCompanyId(COMPANY_ID))
.thenReturn(List.<Object[]>of(new Object[]{1L, 4.0}));
CompanyKnowledgeOverviewResponse result =
service.getCompanyKnowledgeOverview(STUDENT_USER_ID, COMPANY_ID);
assertThat(result.getKnowledgeState()).isEqualTo("CONFIGURED");
assertThat(result.getKnowledge().getCulture()).isEqualTo("Culture");
assertThat(result.getKnowledge().getWorkPolicies()).isEqualTo("Policies");
assertThat(result.getKnowledge().getOnboardingGuide()).isEqualTo("Guide");
assertThat(result.getKnowledge().getUpdatedAt()).isEqualTo(UPDATED_AT);
assertThat(result.getKnowledge().getVersion()).isNull();
assertThat(result.getReviewSummary().getState()).isEqualTo("INSUFFICIENT");
assertThat(result.getReviewSummary().getTotalReviews()).isEqualTo(1);
verify(reviewRepository, never()).findPublishedRatingDistributionByCompanyId(any());
}
@Test
void overview_withThreePublicReviews_shouldReturnAggregatesAndAllEightDimensions() {
stubStudent();
when(companyRepository.findById(COMPANY_ID)).thenReturn(Optional.of(company()));
when(knowledgeRepository.findByCompanyId(COMPANY_ID)).thenReturn(Optional.empty());
when(reviewRepository.countAndAveragePublishedRatingByCompanyId(COMPANY_ID))
.thenReturn(List.<Object[]>of(new Object[]{3L, 4.333}));
when(reviewRepository.findPublishedRatingDistributionByCompanyId(COMPANY_ID))
.thenReturn(List.of(
new Object[]{3, 1L},
new Object[]{4, 1L},
new Object[]{5, 1L}));
when(reviewRepository.findPublishedTagsByCompanyId(COMPANY_ID))
.thenReturn(List.of(
"[\"beta\",\"team\"]",
"[\"alpha\",\"beta\"]"));
when(dimensionScoreRepository.aggregatePublishedDimensionsByCompanyId(COMPANY_ID))
.thenReturn(List.of(
new Object[]{"TEAM_ATMOSPHERE", 4.0, 3L},
new Object[]{"GROWTH", 4.5, 2L}));
CompanyKnowledgeOverviewResponse result =
service.getCompanyKnowledgeOverview(STUDENT_USER_ID, COMPANY_ID);
ReviewSummaryView summary = result.getReviewSummary();
assertThat(summary.getState()).isEqualTo("SUFFICIENT");
assertThat(summary.getTotalReviews()).isEqualTo(3);
assertThat(summary.getAverageRating()).isEqualTo(4.3);
assertThat(summary.getRatingDistribution())
.containsEntry("rating1", 0)
.containsEntry("rating2", 0)
.containsEntry("rating3", 1)
.containsEntry("rating4", 1)
.containsEntry("rating5", 1);
assertThat(summary.getTopTags()).containsExactly("beta", "alpha", "team");
assertThat(summary.getDimensionSummary()).hasSize(ReviewDimension.values().length);
assertThat(summary.getDimensionSummary().get("TEAM_ATMOSPHERE").getAverageScore())
.isEqualTo(4.0);
assertThat(summary.getDimensionSummary().get("TEAM_ATMOSPHERE").getSampleCount())
.isEqualTo(3L);
assertThat(summary.getDimensionSummary().get("TEAM_ATMOSPHERE").getDataState())
.isEqualTo("SUFFICIENT");
assertThat(summary.getDimensionSummary().get("GROWTH").getDataState())
.isEqualTo("INSUFFICIENT");
assertThat(summary.getDimensionSummary().get("AUTHENTICITY").getSampleCount()).isZero();
assertThat(summary.getDimensionSummary().get("AUTHENTICITY").getAverageScore()).isNull();
assertThat(summary.getDimensionSummary().get("AUTHENTICITY").getDataState())
.isEqualTo("INSUFFICIENT");
}
@Test
void overview_companyNotFound_shouldReturnCompanyNotFound() {
stubStudent();
when(companyRepository.findById(COMPANY_ID)).thenReturn(Optional.empty());
assertThatThrownBy(() -> service.getCompanyKnowledgeOverview(STUDENT_USER_ID, COMPANY_ID))
.isInstanceOf(BusinessException.class)
.extracting(ex -> ((BusinessException) ex).getErrorCodeInt())
.isEqualTo(ErrorCode.COMPANY_NOT_FOUND.getCode());
}
@Test
void overview_withNonStudentRole_shouldReturnForbidden() {
when(userRepository.findById(STUDENT_USER_ID)).thenReturn(Optional.of(
User.builder().id(STUDENT_USER_ID).roles(Set.of(User.Role.COMPANY)).build()));
assertThatThrownBy(() -> service.getCompanyKnowledgeOverview(STUDENT_USER_ID, COMPANY_ID))
.isInstanceOf(BusinessException.class)
.extracting(ex -> ((BusinessException) ex).getErrorCodeInt())
.isEqualTo(ErrorCode.FORBIDDEN.getCode());
verifyNoInteractions(companyRepository, knowledgeRepository);
}
@Test
void companyKnowledge_withNonCompanyRole_shouldReturnForbidden() {
when(userRepository.findById(COMPANY_USER_ID)).thenReturn(Optional.of(
User.builder().id(COMPANY_USER_ID).roles(Set.of(User.Role.STUDENT)).build()));
assertThatThrownBy(() -> service.getCompanyKnowledge(COMPANY_USER_ID))
.isInstanceOf(BusinessException.class)
.extracting(ex -> ((BusinessException) ex).getErrorCodeInt())
.isEqualTo(ErrorCode.FORBIDDEN.getCode());
verifyNoInteractions(companyRepository, knowledgeRepository);
}
private void stubCompanyOwner() {
when(userRepository.findById(COMPANY_USER_ID)).thenReturn(Optional.of(
User.builder().id(COMPANY_USER_ID).roles(Set.of(User.Role.COMPANY)).build()));
when(companyRepository.findByOwnerUserId(COMPANY_USER_ID)).thenReturn(Optional.of(company()));
}
private void stubStudent() {
when(userRepository.findById(STUDENT_USER_ID)).thenReturn(Optional.of(
User.builder().id(STUDENT_USER_ID).roles(Set.of(User.Role.STUDENT)).build()));
}
private Company company() {
return Company.builder()
.id(COMPANY_ID)
.ownerUserId(COMPANY_USER_ID)
.name("Example Corp")
.logoUrl("https://example.com/logo.png")
.description("Description")
.industry("Software")
.website("https://example.com")
.address("Shanghai")
.verified(true)
.build();
}
private CompanyKnowledge knowledge(
Long id,
String culture,
String workPolicies,
String onboardingGuide,
Long version,
LocalDateTime updatedAt) {
return CompanyKnowledge.builder()
.id(id)
.companyId(COMPANY_ID)
.culture(culture)
.workPolicies(workPolicies)
.onboardingGuide(onboardingGuide)
.version(version)
.updatedAt(updatedAt)
.build();
}
private CompanyKnowledgeUpdateRequest request(
String culture, String workPolicies, String onboardingGuide, Long version) {
CompanyKnowledgeUpdateRequest request = new CompanyKnowledgeUpdateRequest();
request.setCulture(culture);
request.setWorkPolicies(workPolicies);
request.setOnboardingGuide(onboardingGuide);
request.setVersion(version);
return request;
}
}
Loading…
Cancel
Save