Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .codegen-notes/TESTING_STACK_NOTE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Tests added for RankingRepositoryImpl.
Assumed testing stack: JUnit 5 (Jupiter) + Mockito. Adjust imports if your project differs.
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
package com.loopers.application.product;

import com.loopers.domain.activity.event.ActivityEvent;
import com.loopers.domain.brand.BrandResult;
import com.loopers.domain.brand.BrandService;
import com.loopers.domain.product.ProductResult;
import com.loopers.domain.product.ProductService;
import com.loopers.domain.ranking.RankingCommand;
import com.loopers.domain.ranking.RankingService;
import com.loopers.domain.user.User;
import com.loopers.domain.user.UserService;
import com.loopers.support.error.BusinessException;
import com.loopers.support.error.CommonErrorType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.task.TaskExecutor;
import org.springframework.util.concurrent.ListenableFutureTask;

import java.time.LocalDate;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.*;

/**
* Test stack: JUnit 5 (Jupiter) + Mockito + AssertJ.
* These tests validate ProductFacade.getProductDetail focusing on the behaviors introduced in the PR diff:
* - Fetching product detail, brand (nullable), and rank (nullable)
* - Publishing ActivityEvent.View asynchronously only when userName is present and resolves to a user
*/
class ProductFacadeTest {

private ProductService productService;
private BrandService brandService;
private RankingService rankingService;
private UserService userService;
private TaskExecutor taskExecutor;
private ApplicationEventPublisher eventPublisher;

private ProductFacade facade;

@BeforeEach
void setUp() {
productService = mock(ProductService.class);
brandService = mock(BrandService.class);
rankingService = mock(RankingService.class);
userService = mock(UserService.class);
eventPublisher = mock(ApplicationEventPublisher.class);

// Synchronous TaskExecutor for deterministic testing of async branch
taskExecutor = runnable -> runnable.run();

facade = new ProductFacade(
productService,
brandService,
rankingService,
userService,
taskExecutor,
eventPublisher
);
}

private static ProductInput.GetProductDetail input(Long productId, String userName) {
// Adapt to actual constructor/factory if different in codebase
return new ProductInput.GetProductDetail(productId, userName);
}

private static ProductResult.GetProductDetail productDetail(Long productId, Long brandId) {
// Adapt to actual constructor/factory if different in codebase
return new ProductResult.GetProductDetail(productId, "name", brandId, "desc", 1000L);
}

private static BrandResult.GetBrand brand(Long brandId) {
// Adapt to actual constructor/factory if different in codebase
return new BrandResult.GetBrand(brandId, "brand");
}

@Nested
@DisplayName("Happy paths")
class HappyPaths {
@Test
@DisplayName("Returns output with brand and rank when present; publishes view event for valid userName")
void returnsOutputWithBrandAndRank_andPublishesEvent() {
// given
Long productId = 10L;
Long brandId = 7L;
Long expectedRank = 5L;
String userName = "jane";

ProductResult.GetProductDetail detail = productDetail(productId, brandId);
when(productService.getProductDetail(productId)).thenReturn(Optional.of(detail));
when(brandService.getBrand(brandId)).thenReturn(Optional.of(brand(brandId)));
when(rankingService.findRank(new RankingCommand.FindRank(LocalDate.now(), productId)))
.thenReturn(Optional.of(expectedRank));
User mockUser = mock(User.class);
when(mockUser.getUserId()).thenReturn(99L);
when(userService.getUser(userName)).thenReturn(Optional.of(mockUser));

// when
ProductOutput.GetProductDetail out = facade.getProductDetail(input(productId, userName));

// then
assertThat(out).isNotNull();

ArgumentCaptor<Object> evt = ArgumentCaptor.forClass(Object.class);
verify(eventPublisher, times(1)).publishEvent(evt.capture());
assertThat(evt.getValue()).isInstanceOf(ActivityEvent.View.class);

// Verify call order through main flow
InOrder inOrder = inOrder(productService, brandService, rankingService);
inOrder.verify(productService).getProductDetail(productId);
inOrder.verify(brandService).getBrand(brandId);
inOrder.verify(rankingService).findRank(any(RankingCommand.FindRank.class));
}

@Test
@DisplayName("Returns output when brand and rank are absent (nulls propagated)")
void returnsOutputWhenBrandAndRankAbsent() {
Long productId = 11L;
Long brandId = 77L;

ProductResult.GetProductDetail detail = productDetail(productId, brandId);
when(productService.getProductDetail(productId)).thenReturn(Optional.of(detail));
when(brandService.getBrand(brandId)).thenReturn(Optional.empty());
when(rankingService.findRank(any(RankingCommand.FindRank.class))).thenReturn(Optional.empty());

ProductOutput.GetProductDetail out = facade.getProductDetail(input(productId, null));

assertThat(out).isNotNull();
verify(eventPublisher, never()).publishEvent(any());
}
}

@Nested
@DisplayName("Guard clauses and edge cases")
class GuardAndEdges {
@Test
@DisplayName("Throws NOT_FOUND when product detail is missing")
void throwsWhenProductMissing() {
Long productId = 404L;
when(productService.getProductDetail(productId)).thenReturn(Optional.empty());

assertThatThrownBy(() -> facade.getProductDetail(input(productId, "user")))
.isInstanceOf(BusinessException.class)
.hasMessageContaining(CommonErrorType.NOT_FOUND.name());
verifyNoInteractions(brandService, rankingService, eventPublisher, userService);
}

@Test
@DisplayName("Does not publish event when userName is null")
void noEventWhenUserNameNull() {
Long productId = 12L;
Long brandId = 1L;

when(productService.getProductDetail(productId)).thenReturn(Optional.of(productDetail(productId, brandId)));
when(brandService.getBrand(brandId)).thenReturn(Optional.of(brand(brandId)));

ProductOutput.GetProductDetail out = facade.getProductDetail(input(productId, null));

assertThat(out).isNotNull();
verify(eventPublisher, never()).publishEvent(any());
}

@Test
@DisplayName("Does not publish event when userName is blank")
void noEventWhenUserNameBlank() {
Long productId = 13L;
Long brandId = 2L;

when(productService.getProductDetail(productId)).thenReturn(Optional.of(productDetail(productId, brandId)));
when(brandService.getBrand(brandId)).thenReturn(Optional.of(brand(brandId)));

ProductOutput.GetProductDetail out = facade.getProductDetail(input(productId, " "));

assertThat(out).isNotNull();
verify(eventPublisher, never()).publishEvent(any());
}

@Test
@DisplayName("Executes async branch but does not publish when user not found")
void asyncBranchNoUser() {
Long productId = 14L;
Long brandId = 3L;

when(productService.getProductDetail(productId)).thenReturn(Optional.of(productDetail(productId, brandId)));
when(brandService.getBrand(brandId)).thenReturn(Optional.of(brand(brandId)));
when(userService.getUser("ghost")).thenReturn(Optional.empty());

ProductOutput.GetProductDetail out = facade.getProductDetail(input(productId, "ghost"));

assertThat(out).isNotNull();
verify(userService, times(1)).getUser("ghost");
verify(eventPublisher, never()).publishEvent(any());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package com.loopers.application.ranking;

import com.loopers.domain.product.ProductResult;
import com.loopers.domain.product.ProductService;
import com.loopers.domain.ranking.RankingCommand;
import com.loopers.domain.ranking.RankingResult;
import com.loopers.domain.ranking.RankingService;
import com.loopers.support.error.BusinessException;
import com.loopers.support.error.CommonErrorType;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;

import java.time.LocalDate;
import java.util.List;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

/**
* Test framework: JUnit Jupiter (JUnit 5) + Mockito.
* These tests validate RankingFacade.searchRankings across happy paths, empty results, and missing product details.
*/
class RankingFacadeTest {

private RankingService rankingService;
private ProductService productService;
private RankingFacade facade;

@BeforeEach
void setUp() {
rankingService = mock(RankingService.class);
productService = mock(ProductService.class);
facade = new RankingFacade(rankingService, productService);
}

@Nested
@DisplayName("searchRankings - command wiring and happy path")
class HappyPath {

@Test
void returnsOutputFromRanksAndProductDetails_whenItemsPresent() {
// Arrange
LocalDate date = LocalDate.of(2024, 12, 25);
int page = 2, size = 3;

// Fake rank items with productIds 11, 22, 33
var rank1 = new RankingResult.SearchRanks.Item(1, 11L);
var rank2 = new RankingResult.SearchRanks.Item(2, 22L);
var rank3 = new RankingResult.SearchRanks.Item(3, 33L);
var ranks = new RankingResult.SearchRanks(date, page, size, 9, List.of(rank1, rank2, rank3));

when(rankingService.searchRanks(any(RankingCommand.SearchRanks.class))).thenReturn(ranks);

var d1 = new ProductResult.GetProductDetail(11L, "p11", "desc11", 100L, 10);
var d2 = new ProductResult.GetProductDetail(22L, "p22", "desc22", 200L, 20);
var d3 = new ProductResult.GetProductDetail(33L, "p33", "desc33", 300L, 30);

when(productService.getProductDetail(11L)).thenReturn(Optional.of(d1));
when(productService.getProductDetail(22L)).thenReturn(Optional.of(d2));
when(productService.getProductDetail(33L)).thenReturn(Optional.of(d3));

var input = new RankingInput.SearchRankings(date, page, size);

// Act
var output = facade.searchRankings(input);

// Assert - command wiring
ArgumentCaptor<RankingCommand.SearchRanks> captor = ArgumentCaptor.forClass(RankingCommand.SearchRanks.class);
verify(rankingService).searchRanks(captor.capture());
var sent = captor.getValue();
assertEquals(date, sent.date());
assertEquals(page, sent.page());
assertEquals(size, sent.size());

// Assert - product details fetched for each rank
verify(productService).getProductDetail(11L);
verify(productService).getProductDetail(22L);
verify(productService).getProductDetail(33L);
verifyNoMoreInteractions(productService);

// Basic sanity on output
assertNotNull(output, "Output must not be null");
}
}

@Nested
@DisplayName("searchRankings - empty items")
class EmptyItems {

@Test
void returnsEmptyOutputAndDoesNotQueryProductService_whenNoItems() {
// Arrange
LocalDate date = LocalDate.of(2024, 1, 1);
int page = 0, size = 10;
var ranks = new RankingResult.SearchRanks(date, page, size, 0, List.of());
when(rankingService.searchRanks(any(RankingCommand.SearchRanks.class))).thenReturn(ranks);

var input = new RankingInput.SearchRankings(date, page, size);

// Act
var output = facade.searchRankings(input);

// Assert
assertNotNull(output, "Output should not be null even when empty");
verifyNoInteractions(productService);
}
}

@Nested
@DisplayName("searchRankings - missing product detail")
class MissingProductDetail {

@Test
void throwsBusinessException_whenProductDetailNotFound() {
// Arrange
LocalDate date = LocalDate.of(2024, 6, 30);
var rank = new RankingResult.SearchRanks.Item(1, 999L);
var ranks = new RankingResult.SearchRanks(date, 0, 1, 1, List.of(rank));
when(rankingService.searchRanks(any(RankingCommand.SearchRanks.class))).thenReturn(ranks);
when(productService.getProductDetail(999L)).thenReturn(Optional.empty());

var input = new RankingInput.SearchRankings(date, 0, 1);

// Act + Assert
BusinessException ex = assertThrows(BusinessException.class, () -> facade.searchRankings(input));
assertEquals(CommonErrorType.NOT_FOUND, ex.getErrorType());
verify(productService).getProductDetail(999L);
}
}
}
Loading