Skip to content
Merged
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
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
testImplementation "org.mockito:mockito-inline:4.+"

// bcrypt
implementation 'at.favre.lib:bcrypt:0.10.2'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.ezcode.codetest.application.language.service;
package org.ezcode.codetest.application.language;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package org.ezcode.codetest.application.submission;

import static org.mockito.BDDMockito.*;

import org.ezcode.codetest.application.submission.dto.event.GitPushStatusEvent;
import org.ezcode.codetest.application.submission.dto.request.github.GitHubPushRequest;
import org.ezcode.codetest.application.submission.model.SubmissionContext;
import org.ezcode.codetest.application.submission.port.ExceptionNotifier;
import org.ezcode.codetest.application.submission.port.GitHubClient;
import org.ezcode.codetest.application.submission.port.SubmissionEventService;
import org.ezcode.codetest.application.submission.service.GitHubPushService;
import org.ezcode.codetest.common.security.util.AESUtil;
import org.ezcode.codetest.domain.submission.exception.SubmissionException;
import org.ezcode.codetest.domain.user.model.entity.UserGithubInfo;
import org.ezcode.codetest.domain.user.service.UserGithubService;
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.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
@DisplayName("깃허브 푸시 서비스 테스트")
public class GitHubPushServiceTest {

@InjectMocks
private GitHubPushService gitHubPushService;

@Mock
private GitHubClient gitHubClient;

@Mock
private UserGithubService userGithubService;

@Mock
private ExceptionNotifier exceptionNotifier;

@Mock
private SubmissionEventService eventService;

@Mock
private AESUtil aesUtil;

@Mock
private UserGithubInfo info;

@Mock
private SubmissionContext ctx;

@Mock
private GitHubPushRequest req;

private Long userId;
private String sessionKey;
private String encryptedToken;
private String decryptedToken;

@BeforeEach
void setUp() {
userId = 1L;
sessionKey = "session-key";
encryptedToken = "encrypted-token";
decryptedToken = "decrypted-token";
}

@Nested
@DisplayName("성공 케이스")
class SuccessCases {

@Test
@DisplayName("GitPushStatus 비활성 -> 아무 동작도 하지 않음")
void noPushWhenDisabled() {

// given
given(ctx.isGitPushStatus()).willReturn(false);

// when
gitHubPushService.pushSolutionToRepo(ctx);

// then
then(eventService).should(never()).publishGitPushStatus(any(GitPushStatusEvent.class));
then(userGithubService).should(never()).getUserGithubInfoById(anyLong());
then(gitHubClient).should(never()).commitAndPushToRepo(any());
then(exceptionNotifier).should(never()).notifyException(anyString(), any());
}

@Test
@DisplayName("오답 제출 -> 아무 동작도 하지 않음")
void noPushWhenNotPassed() {

// given
given(ctx.isGitPushStatus()).willReturn(true);
given(ctx.isPassed()).willReturn(false);

// when
gitHubPushService.pushSolutionToRepo(ctx);

// then
then(eventService).should(never()).publishGitPushStatus(any(GitPushStatusEvent.class));
then(userGithubService).should(never()).getUserGithubInfoById(anyLong());
then(gitHubClient).should(never()).commitAndPushToRepo(any());
then(exceptionNotifier).should(never()).notifyException(anyString(), any());
}

@Test
@DisplayName("활성화 & 정답 제출 -> 메서드 실행 및 시작 & 성공 이벤트 발행")
void pushSuccess() throws Exception {

// given
given(ctx.isGitPushStatus()).willReturn(true);
given(ctx.isPassed()).willReturn(true);

given(ctx.getSessionKey()).willReturn(sessionKey);
given(ctx.getUserId()).willReturn(userId);

given(userGithubService.getUserGithubInfoById(userId)).willReturn(info);
given(info.getGithubAccessToken()).willReturn(encryptedToken);
given(aesUtil.decrypt(encryptedToken)).willReturn(decryptedToken);

// when & then
try (var ms = mockStatic(GitHubPushRequest.class)) {
ms.when(() -> GitHubPushRequest.of(ctx, info, decryptedToken)).thenReturn(req);

gitHubPushService.pushSolutionToRepo(ctx);

then(eventService).should().publishGitPushStatus(GitPushStatusEvent.started(sessionKey));
then(gitHubClient).should().commitAndPushToRepo(req);
then(eventService).should().publishGitPushStatus(GitPushStatusEvent.succeeded(sessionKey));
then(exceptionNotifier).should(never()).notifyException(anyString(), any());
}
}
}

@Nested
@DisplayName("실패 케이스")
class FailureCases {

@Test
@DisplayName("Git Push 실패 -> 알림 호출 & 실패 이벤트 발행")
void pushFailure() throws Exception {

// given
given(ctx.isGitPushStatus()).willReturn(true);
given(ctx.isPassed()).willReturn(true);
given(ctx.getSessionKey()).willReturn(sessionKey);
given(ctx.getUserId()).willReturn(userId);
given(userGithubService.getUserGithubInfoById(userId)).willReturn(info);
given(info.getGithubAccessToken()).willReturn(encryptedToken);
given(aesUtil.decrypt(encryptedToken)).willReturn(decryptedToken);

willThrow(SubmissionException.class).given(gitHubClient).commitAndPushToRepo(req);

// when & then
try (var ms = mockStatic(GitHubPushRequest.class)) {
ms.when(() -> GitHubPushRequest.of(ctx, info, decryptedToken))
.thenReturn(req);

gitHubPushService.pushSolutionToRepo(ctx);

then(eventService).should().publishGitPushStatus(GitPushStatusEvent.started(sessionKey));
then(exceptionNotifier).should().notifyException(eq("commitAndPush"), any(SubmissionException.class));
then(eventService).should().publishGitPushStatus(GitPushStatusEvent.failed(sessionKey));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package org.ezcode.codetest.application.submission;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.BDDMockito.*;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import org.ezcode.codetest.application.submission.dto.event.TestcaseEvaluatedEvent;
import org.ezcode.codetest.application.submission.model.JudgeResult;
import org.ezcode.codetest.application.submission.model.SubmissionContext;
import org.ezcode.codetest.application.submission.port.ExceptionNotifier;
import org.ezcode.codetest.application.submission.port.JudgeClient;
import org.ezcode.codetest.application.submission.port.SubmissionEventService;
import org.ezcode.codetest.application.submission.service.JudgementService;
import org.ezcode.codetest.domain.submission.exception.SubmissionException;
import org.ezcode.codetest.domain.submission.exception.code.SubmissionExceptionCode;
import org.ezcode.codetest.domain.submission.service.SubmissionDomainService;
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.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;

@ExtendWith(MockitoExtension.class)
@DisplayName("채점 서비스 비동기 로직 테스트")
public class JudgementServiceAsyncTest {

@InjectMocks
private JudgementService judgementService;

@Mock
private SubmissionDomainService submissionDomainService;

@Mock
private SubmissionEventService submissionEventService;

@Mock
private JudgeClient judgeClient;

@Mock
private ExceptionNotifier exceptionNotifier;

@Mock
private SubmissionContext ctx;

@Mock
private JudgeResult judgeResult;

@Mock
private CountDownLatch latch;
private int testcaseCount;
private String sessionKey;
private AtomicBoolean notified;
private final Executor directExecutor = Runnable::run;

@BeforeEach
void setup() {
testcaseCount = 5;
sessionKey = "session-key";
notified = new AtomicBoolean(false);
ReflectionTestUtils.setField(judgementService, "judgeTestcaseExecutor", directExecutor);
}

@Nested
@DisplayName("성공 케이스")
class SuccessCases {

@Test
@DisplayName("runTestcases -> TestcaseEvaludatedEvent를 테스트케이스 개수만큼 발행")
void runTestcasesPublishesEvaluations() throws InterruptedException {

// given
given(ctx.getTestcaseCount()).willReturn(testcaseCount);
given(ctx.latch()).willReturn(latch);
doReturn(true).when(latch).await(anyLong(), any(TimeUnit.class));
given(judgeClient.submitAndGetToken(any())).willReturn("t1", "t2", "t3", "t4", "t5");
given(judgeClient.pollUntilDone(anyString())).willReturn(judgeResult);

given(submissionDomainService.handleEvaluationAndUpdateStats(any(), eq(ctx)))
.willReturn(true)
.willReturn(false)
.willReturn(true)
.willReturn(false)
.willReturn(true);

// when
judgementService.runTestcases(ctx);

// then
then(submissionEventService).should(times(testcaseCount))
.publishTestcaseUpdate(any(TestcaseEvaluatedEvent.class));
}
}

@Nested
@DisplayName("실패 케이스")
class FailureCases {

@Test
@DisplayName("타임아웃 -> TESTCASE_TIMEOUT 예외 발생")
void timeoutThrowsSubmissionException() throws InterruptedException {

// given
given(ctx.getTestcaseCount()).willReturn(testcaseCount);
given(ctx.latch()).willReturn(latch);
doReturn(false).when(latch).await(anyLong(), any(TimeUnit.class));

// when & then
assertThatThrownBy(() -> judgementService.runTestcases(ctx))
.isInstanceOf(SubmissionException.class)
.hasMessage(SubmissionExceptionCode.TESTCASE_TIMEOUT.getMessage());
}

@Test
@DisplayName("judgeClient 예외 발생 -> 알림 호출 1회 & 에러 이벤트 발행")
void runTestcaseAsyncExceptionNotifies() throws InterruptedException {

// given
given(ctx.getSessionKey()).willReturn(sessionKey);
given(ctx.getTestcaseCount()).willReturn(testcaseCount);
given(ctx.latch()).willReturn(latch);
doReturn(true).when(latch).await(anyLong(), any(TimeUnit.class));

given(ctx.notified()).willReturn(notified);
doNothing().when(ctx).countDown();
given(judgeClient.submitAndGetToken(any()))
.willThrow(new SubmissionException(SubmissionExceptionCode.COMPILE_SERVER_ERROR));

// when
judgementService.runTestcases(ctx);

// then
then(submissionEventService).should(times(1)).publishSubmissionError(
argThat(ev ->
ev.sessionKey().equals(ctx.getSessionKey()) &&
ev.code().equals(SubmissionExceptionCode.COMPILE_SERVER_ERROR)
)
);
then(exceptionNotifier).should(times(1)).notifyException(
eq("runTestcaseAsync"), any(SubmissionException.class)
);
then(ctx).should(times(5)).countDown();
}
}
}
Loading