Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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