Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 과제 히스토리 도메인 로직 및 깃허브 클라이언트 로직 변경 #639

Merged
merged 15 commits into from
Aug 18, 2024
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
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.gdschongik.gdsc.domain.study.domain;

import static com.gdschongik.gdsc.domain.study.domain.AssignmentSubmissionStatus.*;
import static com.gdschongik.gdsc.domain.study.domain.SubmissionFailureType.*;
import static com.gdschongik.gdsc.global.exception.ErrorCode.*;

import com.gdschongik.gdsc.domain.common.model.BaseEntity;
import com.gdschongik.gdsc.domain.member.domain.Member;
import com.gdschongik.gdsc.global.exception.CustomException;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
Expand All @@ -14,6 +17,7 @@
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import java.time.LocalDateTime;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
Expand Down Expand Up @@ -44,6 +48,8 @@ public class AssignmentHistory extends BaseEntity {

private Long contentLength;

private LocalDateTime committedAt;

@Enumerated(EnumType.STRING)
private AssignmentSubmissionStatus submissionStatus;

Expand All @@ -54,27 +60,50 @@ public class AssignmentHistory extends BaseEntity {
private AssignmentHistory(
StudyDetail studyDetail,
Member member,
String submissionLink,
String commitHash,
Long contentLength,
AssignmentSubmissionStatus submissionStatus) {
AssignmentSubmissionStatus submissionStatus,
SubmissionFailureType submissionFailureType) {
this.studyDetail = studyDetail;
this.member = member;
this.submissionLink = submissionLink;
this.commitHash = commitHash;
this.contentLength = contentLength;
this.submissionStatus = submissionStatus;
this.submissionFailureType = submissionFailureType;
}

public static AssignmentHistory create(StudyDetail studyDetail, Member member) {
return AssignmentHistory.builder()
.studyDetail(studyDetail)
.member(member)
.submissionStatus(AssignmentSubmissionStatus.PENDING)
.submissionStatus(FAILURE)
.submissionFailureType(NOT_SUBMITTED)
.build();
}

// 데이터 조회 로직

public boolean isSubmitted() {
return submissionStatus == SUCCESS || submissionStatus == FAILURE;
return submissionFailureType != NOT_SUBMITTED;
}

// 데이터 변경 로직

public void success(String submissionLink, String commitHash, Long contentLength, LocalDateTime committedAt) {
this.submissionLink = submissionLink;
this.commitHash = commitHash;
this.contentLength = contentLength;
this.committedAt = committedAt;
this.submissionStatus = SUCCESS;
this.submissionFailureType = NONE;
}

public void fail(SubmissionFailureType submissionFailureType) {
if (submissionFailureType == NOT_SUBMITTED || submissionFailureType == NONE) {
throw new CustomException(ASSIGNMENT_INVALID_FAILURE_TYPE);
}

this.submissionLink = null;
this.commitHash = null;
this.contentLength = null;
this.committedAt = null;
this.submissionStatus = FAILURE;
this.submissionFailureType = submissionFailureType;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@
@Getter
@RequiredArgsConstructor
public enum AssignmentSubmissionStatus {
PENDING("제출 전"),
// TODO: 클라이언트 응답에는 PENDING 상태 필요하므로, 추후 응답용 enum 클래스 생성 구현
FAILURE("제출 실패"),
SUCCESS("제출 성공"),
CANCELLED("과제 휴강"); // TODO: 제거 및 DB에서 삭제
SUCCESS("제출 성공");

private final String value;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
@Getter
@RequiredArgsConstructor
public enum SubmissionFailureType {
NOT_SUBMITTED("미제출"),
NONE("실패 없음"), // 제출상태 성공 시 사용
NOT_SUBMITTED("미제출"), // 기본값
WORD_COUNT_INSUFFICIENT("글자수 부족"),
LOCATION_UNIDENTIFIABLE("위치 확인불가");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ public enum ErrorCode {
ORDER_FINAL_PAYMENT_AMOUNT_MISMATCH(HttpStatus.CONFLICT, "주문 최종결제금액은 주문총액에서 할인금액을 뺀 값이어야 합니다."),

// Assignment
ASSIGNMENT_CAN_NOT_BE_UPDATED(HttpStatus.CONFLICT, "휴강인 과제는 수정할 수 없습니다."),
ASSIGNMENT_INVALID_FAILURE_TYPE(HttpStatus.CONFLICT, "유효하지 않은 제출 실패사유입니다."),
ASSIGNMENT_DEADLINE_INVALID(HttpStatus.CONFLICT, "과제 마감 기한이 현재보다 빠릅니다."),
ASSIGNMENT_STUDY_NOT_APPLIED(HttpStatus.CONFLICT, "해당 스터디에 대한 수강신청 기록이 존재하지 않습니다."),
ASSIGNMENT_SUBMIT_NOT_STARTED(HttpStatus.CONFLICT, "아직 과제가 시작되지 않았습니다."),
Expand All @@ -158,6 +158,9 @@ public enum ErrorCode {

// Github
GITHUB_REPOSITORY_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 레포지토리입니다."),
GITHUB_CONTENT_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 파일입니다."),
GITHUB_FILE_READ_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "깃허브 파일 읽기에 실패했습니다."),
GITHUB_COMMIT_DATE_FETCH_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "깃허브 커밋 날짜 조회에 실패했습니다."),
GITHUB_ASSIGNMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 과제 파일입니다."),
;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package com.gdschongik.gdsc.infra.github.client;

import static com.gdschongik.gdsc.global.common.constant.GithubConstant.*;
import static com.gdschongik.gdsc.global.exception.ErrorCode.*;

import com.gdschongik.gdsc.global.exception.CustomException;
import com.gdschongik.gdsc.global.exception.ErrorCode;
import com.gdschongik.gdsc.infra.github.dto.response.GithubAssignmentSubmissionResponse;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import lombok.RequiredArgsConstructor;
import org.kohsuke.github.GHCommit;
import org.kohsuke.github.GHContent;
Expand All @@ -25,35 +27,55 @@ public GHRepository getRepository(String ownerRepo) {
try {
return github.getRepository(ownerRepo);
} catch (IOException e) {
throw new CustomException(ErrorCode.GITHUB_REPOSITORY_NOT_FOUND);
throw new CustomException(GITHUB_REPOSITORY_NOT_FOUND);
}
}

public GithubAssignmentSubmissionResponse getLatestAssignmentSubmission(String repo, int week) {
GHRepository ghRepository = getRepository(repo);
String assignmentPath = GITHUB_ASSIGNMENT_PATH.formatted(week);

// GHContent#getSize() 의 경우 한글 문자열을 byte 단위로 계산하기 때문에, 직접 content를 읽어서 길이를 계산
GHContent ghContent = getFileContent(ghRepository, assignmentPath);
String content = readFileContent(ghContent);

GHCommit ghLatestCommit = ghRepository
.queryCommits()
.path(assignmentPath)
.list()
.withPageSize(1)
.iterator()
.next();

LocalDateTime committedAt = getCommitDate(ghLatestCommit)
.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime();

return new GithubAssignmentSubmissionResponse(ghLatestCommit.getSHA1(), content.length(), committedAt);
}

private GHContent getFileContent(GHRepository ghRepository, String filePath) {
try {
return ghRepository.getFileContent(filePath);
} catch (IOException e) {
throw new CustomException(GITHUB_CONTENT_NOT_FOUND);
}
}

private String readFileContent(GHContent ghContent) {
try (InputStream inputStream = ghContent.read()) {
return new String(inputStream.readAllBytes());
} catch (IOException e) {
throw new CustomException(GITHUB_FILE_READ_FAILED);
}
}

private Date getCommitDate(GHCommit ghLatestCommit) {
try {
GHRepository ghRepository = getRepository(repo);
String assignmentPath = GITHUB_ASSIGNMENT_PATH.formatted(week);

// GHContent#getSize() 의 경우 한글 문자열을 byte 단위로 계산하기 때문에, 직접 content를 읽어서 길이를 계산
GHContent ghContent = ghRepository.getFileContent(assignmentPath);
String content = new String(ghContent.read().readAllBytes());

GHCommit ghLatestCommit = ghRepository
.queryCommits()
.path(assignmentPath)
.list()
.toList()
.get(0);

LocalDateTime committedAt = ghLatestCommit
.getCommitDate()
.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime();

return new GithubAssignmentSubmissionResponse(ghLatestCommit.getSHA1(), content.length(), committedAt);
return ghLatestCommit.getCommitDate();
} catch (IOException e) {
throw new CustomException(ErrorCode.GITHUB_ASSIGNMENT_NOT_FOUND);
throw new CustomException(GITHUB_COMMIT_DATE_FETCH_FAILED);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package com.gdschongik.gdsc.domain.study.domain;

import static com.gdschongik.gdsc.global.common.constant.StudyConstant.*;
import static com.gdschongik.gdsc.global.exception.ErrorCode.*;
import static org.assertj.core.api.Assertions.*;

import com.gdschongik.gdsc.domain.member.domain.Member;
import com.gdschongik.gdsc.domain.recruitment.domain.vo.Period;
import com.gdschongik.gdsc.global.exception.CustomException;
import com.gdschongik.gdsc.helper.FixtureHelper;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

class AssignmentHistoryTest {

FixtureHelper fixtureHelper = new FixtureHelper();

private Member createMember(Long id) {
return fixtureHelper.createAssociateMember(id);
}

private Study createStudyWithMentor(Long mentorId) {
Period period = Period.createPeriod(STUDY_START_DATETIME, STUDY_END_DATETIME);
Period applicationPeriod =
Period.createPeriod(STUDY_START_DATETIME.minusDays(7), STUDY_START_DATETIME.minusDays(1));
return fixtureHelper.createStudyWithMentor(mentorId, period, applicationPeriod);
}

private StudyDetail createStudyDetailWithAssignment(Study study) {
return fixtureHelper.createStudyDetailWithAssignment(
study, STUDY_DETAIL_START_DATETIME, STUDY_DETAIL_END_DATETIME, STUDY_ASSIGNMENT_DEADLINE_DATETIME);
}

@Nested
class 빈_과제이력_생성할때 {

@Test
void 제출상태는_FAILURE이다() {
// given
Member member = createMember(1L);
Study study = createStudyWithMentor(1L);
StudyDetail studyDetail = createStudyDetailWithAssignment(study);

// when
AssignmentHistory assignmentHistory = AssignmentHistory.create(studyDetail, member);

// then
assertThat(assignmentHistory.getSubmissionStatus()).isEqualTo(AssignmentSubmissionStatus.FAILURE);
}

@Test
void 실패사유는_NOT_SUBMITTED이다() {
// given
Member member = createMember(1L);
Study study = createStudyWithMentor(1L);
StudyDetail studyDetail = createStudyDetailWithAssignment(study);

// when
AssignmentHistory assignmentHistory = AssignmentHistory.create(studyDetail, member);

// then
assertThat(assignmentHistory.getSubmissionFailureType()).isEqualTo(SubmissionFailureType.NOT_SUBMITTED);
}
}

@Nested
class 과제이력_제출_성공할때 {

@Test
void 제출상태는_SUCCESS이다() {
// given
Member member = createMember(1L);
Study study = createStudyWithMentor(1L);
StudyDetail studyDetail = createStudyDetailWithAssignment(study);
AssignmentHistory assignmentHistory = AssignmentHistory.create(studyDetail, member);

// when
assignmentHistory.success(SUBMISSION_LINK, COMMIT_HASH, CONTENT_LENGTH, COMMITTED_AT);
}

@Test
void 실패사유는_NONE이다() {
// given
Member member = createMember(1L);
Study study = createStudyWithMentor(1L);
StudyDetail studyDetail = createStudyDetailWithAssignment(study);
AssignmentHistory assignmentHistory = AssignmentHistory.create(studyDetail, member);

// when
assignmentHistory.success(SUBMISSION_LINK, COMMIT_HASH, CONTENT_LENGTH, COMMITTED_AT);

// then
assertThat(assignmentHistory.getSubmissionFailureType()).isEqualTo(SubmissionFailureType.NONE);
}
}

@Nested
class 과제이력_제출_실패할때 {

@Test
void 제출상태는_FAILURE이다() {
// given
Member member = createMember(1L);
Study study = createStudyWithMentor(1L);
StudyDetail studyDetail = createStudyDetailWithAssignment(study);
AssignmentHistory assignmentHistory = AssignmentHistory.create(studyDetail, member);
assignmentHistory.success(SUBMISSION_LINK, COMMIT_HASH, CONTENT_LENGTH, COMMITTED_AT);

// when
assignmentHistory.fail(SubmissionFailureType.WORD_COUNT_INSUFFICIENT);

// then
assertThat(assignmentHistory.getSubmissionStatus()).isEqualTo(AssignmentSubmissionStatus.FAILURE);
}

@Test
void 실패사유는_NOT_SUBMITTED가_될수없다() {
// given
Member member = createMember(1L);
Study study = createStudyWithMentor(1L);
StudyDetail studyDetail = createStudyDetailWithAssignment(study);
AssignmentHistory assignmentHistory = AssignmentHistory.create(studyDetail, member);
assignmentHistory.success(SUBMISSION_LINK, COMMIT_HASH, CONTENT_LENGTH, COMMITTED_AT);

// when, then
assertThatThrownBy(() -> assignmentHistory.fail(SubmissionFailureType.NOT_SUBMITTED))
.isInstanceOf(CustomException.class)
.hasMessageContaining(ASSIGNMENT_INVALID_FAILURE_TYPE.getMessage());
}

@Test
void 실패사유는_NONE이_될수없다() {
// given
Member member = createMember(1L);
Study study = createStudyWithMentor(1L);
StudyDetail studyDetail = createStudyDetailWithAssignment(study);
AssignmentHistory assignmentHistory = AssignmentHistory.create(studyDetail, member);
assignmentHistory.success(SUBMISSION_LINK, COMMIT_HASH, CONTENT_LENGTH, COMMITTED_AT);

// when, then
assertThatThrownBy(() -> assignmentHistory.fail(SubmissionFailureType.NONE))
.isInstanceOf(CustomException.class)
.hasMessageContaining(ASSIGNMENT_INVALID_FAILURE_TYPE.getMessage());
}

@Test
void 기존_제출정보는_삭제된다() {
// given
Member member = createMember(1L);
Study study = createStudyWithMentor(1L);
StudyDetail studyDetail = createStudyDetailWithAssignment(study);
AssignmentHistory assignmentHistory = AssignmentHistory.create(studyDetail, member);
assignmentHistory.success(SUBMISSION_LINK, COMMIT_HASH, CONTENT_LENGTH, COMMITTED_AT);

// when
assignmentHistory.fail(SubmissionFailureType.WORD_COUNT_INSUFFICIENT);

// then
assertThat(assignmentHistory.getSubmissionLink()).isNull();
assertThat(assignmentHistory.getCommitHash()).isNull();
assertThat(assignmentHistory.getContentLength()).isNull();
assertThat(assignmentHistory.getCommittedAt()).isNull();
}
}
}
Loading