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 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 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("", 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.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.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.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.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.of(new Object[]{3L, BigDecimal.valueOf(4.0)})); when(reviewRepository.findPublishedRatingDistributionByCompanyId(COMPANY_ID)) .thenReturn(List.of()); List 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.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; } }