You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

448 lines
22 KiB

package com.mydreamjob.onboarding;
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 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.ArrayList;
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_withExactlyTwoPublicReviews_shouldReturnInsufficientWithoutAggregates() {
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[]{2L, BigDecimal.valueOf(4.0)}));
ReviewSummaryView summary = service.getCompanyKnowledgeOverview(
STUDENT_USER_ID, COMPANY_ID).getReviewSummary();
assertThat(summary.getState()).isEqualTo("INSUFFICIENT");
assertThat(summary.getTotalReviews()).isEqualTo(2);
assertThat(summary.getAverageRating()).isNull();
assertThat(summary.getRatingDistribution()).isNull();
assertThat(summary.getTopTags()).isNull();
assertThat(summary.getDimensionSummary()).isNull();
verify(reviewRepository, never()).findPublishedRatingDistributionByCompanyId(any());
verify(reviewRepository, never()).findPublishedTagsByCompanyId(any());
verifyNoInteractions(dimensionScoreRepository);
}
@Test
void overview_withMoreThanTenDistinctTags_shouldReturnOnlyTopTenDeterministically() {
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, BigDecimal.valueOf(4.0)}));
when(reviewRepository.findPublishedRatingDistributionByCompanyId(COMPANY_ID))
.thenReturn(List.of());
List<String> rawTags = new ArrayList<>();
for (int index = 1; index <= 11; index++) {
rawTags.add("[\"tag-" + String.format("%02d", index) + "\"]");
}
when(reviewRepository.findPublishedTagsByCompanyId(COMPANY_ID)).thenReturn(rawTags);
when(dimensionScoreRepository.aggregatePublishedDimensionsByCompanyId(COMPANY_ID))
.thenReturn(List.of());
ReviewSummaryView summary = service.getCompanyKnowledgeOverview(
STUDENT_USER_ID, COMPANY_ID).getReviewSummary();
assertThat(summary.getTopTags()).containsExactly(
"tag-01", "tag-02", "tag-03", "tag-04", "tag-05",
"tag-06", "tag-07", "tag-08", "tag-09", "tag-10");
}
@Test
void overview_withRecursivelyQuotedJsonTags_shouldParseTheTags() throws Exception {
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, BigDecimal.valueOf(4.0)}));
when(reviewRepository.findPublishedRatingDistributionByCompanyId(COMPANY_ID))
.thenReturn(List.of());
ObjectMapper mapper = new ObjectMapper();
String onceQuoted = mapper.writeValueAsString("[\"nested-tag\"]");
String twiceQuoted = mapper.writeValueAsString(onceQuoted);
when(reviewRepository.findPublishedTagsByCompanyId(COMPANY_ID))
.thenReturn(List.of(twiceQuoted));
when(dimensionScoreRepository.aggregatePublishedDimensionsByCompanyId(COMPANY_ID))
.thenReturn(List.of());
ReviewSummaryView summary = service.getCompanyKnowledgeOverview(
STUDENT_USER_ID, COMPANY_ID).getReviewSummary();
assertThat(summary.getTopTags()).containsExactly("nested-tag");
}
@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;
}
}