diff --git a/gradle.properties b/gradle.properties index 5dbe19dc28..4ee270c3ab 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,4 @@ org.gradle.jvmargs=-Dfile.encoding=UTF-8 org.gradle.console=plain -org.gradle.java.home=/opt/homebrew/Cellar/openjdk@11/11.0.26/libexec/openjdk.jdk/Contents/Home \ No newline at end of file +org.gradle.java.home=/opt/homebrew/Cellar/openjdk@11/11.0.26/libexec/openjdk.jdk/Contents/Home +# 넵, 위 설정은 IDE 에서 Java 실행 환경을 위해 필요합니다. \ No newline at end of file diff --git a/src/main/java/nextstep/README.md b/src/main/java/nextstep/README.md index 1bdd624aa3..b40185ac6f 100644 --- a/src/main/java/nextstep/README.md +++ b/src/main/java/nextstep/README.md @@ -9,3 +9,27 @@ - [x] QnaService의 비지니스 로직을 도메인 모델로 이동하는 리팩터링을 진행할 때 TDD로 구현한다. - [x] QnaService의 deleteQuestion() 메서드에 대한 단위 테스트는 src/test/java 폴더 nextstep.qna.service.QnaServiceTest이다. - [x]도메인 모델로 로직을 이동한 후에도 QnaServiceTest의 모든 테스트는 통과해야 한다. + +## # Step1 피드백 + +- [x] `gradle.properties` 의 "org.gradle.java.home" 문의 +- [x] "answers" 를 관리하는 일급컬렉션 추가 +- [x] 테스트 이름 구체적으로 명시 + +## # Step2 요구사항 + +- [x] 과정(Course)은 기수 단위로 운영하며, 여러 개의 강의(Session)를 가질 수 있다. +- [x] 강의는 시작일과 종료일을 가진다. +- [x] 강의는 강의 커버 이미지 정보를 가진다. + - [x] 이미지 크기는 1MB 이하여야 한다. + - [x] 이미지 타입은 gif, jpg(jpeg 포함), png, svg 만 허용한다. + - [x] 이미지의 width는 300픽셀, height는 200픽셀 이상이어야 하며, width와 height의 비율은 3:2여야 한다. +- [x] 강의는 무료 강의와 유료 강으로 나뉜다. + - [x] 무료 강의는 최대 수강 인원 제한이 없다. + - [x] 유료 강의는 강의 최대 수강 인원을 초과할 수 없다. + * 유료 강의는 수강생이 결제한 금액과 수강료가 일치할 때 수강 신청이 가능하다. +- [x] 강의 상태는 준비중, 모집중, 종료 3가지 상태를 가진다. +- [x] 강의 수강신청은 강의 상태가 모집중일 때만 가능하다. + +* 유료 강의의 경우 결제는 이미 완료한 것으로 가정하고 이후 과정을 구현한다. + * 결제를 완료한 결제 정보는 payments 모듈을 통해 관리되며, 결제 정보는 Payment 객체에 담겨 반환된다. diff --git a/src/main/java/nextstep/courses/domain/AttendeeList.java b/src/main/java/nextstep/courses/domain/AttendeeList.java new file mode 100644 index 0000000000..8bb731462c --- /dev/null +++ b/src/main/java/nextstep/courses/domain/AttendeeList.java @@ -0,0 +1,22 @@ +package nextstep.courses.domain; + +import java.util.ArrayList; +import java.util.List; + +import nextstep.users.domain.NsUser; + +public class AttendeeList { + private final List attendees; + + public AttendeeList() { + this.attendees = new ArrayList<>(); + } + + public void add(NsUser attendee) { + attendees.add(attendee); + } + + public Long size() { + return (long) attendees.size(); + } +} diff --git a/src/main/java/nextstep/courses/domain/Course.java b/src/main/java/nextstep/courses/domain/Course.java index 0f69716043..df1f994e68 100644 --- a/src/main/java/nextstep/courses/domain/Course.java +++ b/src/main/java/nextstep/courses/domain/Course.java @@ -1,5 +1,6 @@ package nextstep.courses.domain; +import java.awt.image.BufferedImage; import java.time.LocalDateTime; public class Course { @@ -13,19 +14,43 @@ public class Course { private LocalDateTime updatedAt; + private SessionList sessions; + + private String courseCoverImageFilePath; + + private CourseCoverImage courseCoverImage; + + private Long maxAttendees; + + private CourseStatus courseStatus; + + private AttendeeList attendees = new AttendeeList(); + public Course() { } - public Course(String title, Long creatorId) { - this(0L, title, creatorId, LocalDateTime.now(), null); + public Course(String title, Long creatorId, String courseCoverImageFilePath, Long maxAttendees) { + this(0L, title, creatorId, LocalDateTime.now(), null, courseCoverImageFilePath, maxAttendees, CourseStatus.PREPARING); + } + + public Course(String title, Long creatorId, String courseCoverImageFilePath, Long maxAttendees, CourseStatus courseStatus) { + this(0L, title, creatorId, LocalDateTime.now(), null, courseCoverImageFilePath, maxAttendees, courseStatus); } - public Course(Long id, String title, Long creatorId, LocalDateTime createdAt, LocalDateTime updatedAt) { + public Course(Long id, String title, Long creatorId, LocalDateTime createdAt, LocalDateTime updatedAt, String courseCoverImageFilePath, Long maxAttendees, CourseStatus courseStatus) { this.id = id; this.title = title; this.creatorId = creatorId; this.createdAt = createdAt; this.updatedAt = updatedAt; + this.courseCoverImageFilePath = courseCoverImageFilePath; + this.courseCoverImage = new CourseCoverImage(courseCoverImageFilePath); + this.maxAttendees = maxAttendees; + this.courseStatus = courseStatus; + } + + public AttendeeList getAttendees() { + return attendees; } public String getTitle() { @@ -40,6 +65,14 @@ public LocalDateTime getCreatedAt() { return createdAt; } + public Long getMaxAttendees() { + return maxAttendees; + } + + public CourseStatus getCourseStatus() { + return courseStatus; + } + @Override public String toString() { return "Course{" + diff --git a/src/main/java/nextstep/courses/domain/CourseCoverImage.java b/src/main/java/nextstep/courses/domain/CourseCoverImage.java new file mode 100644 index 0000000000..5e6a50e7eb --- /dev/null +++ b/src/main/java/nextstep/courses/domain/CourseCoverImage.java @@ -0,0 +1,59 @@ +package nextstep.courses.domain; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.util.Arrays; + +import javax.imageio.ImageIO; + +public class CourseCoverImage { + + private static final String[] SUPPORTED_IMAGE_EXTENSIONS = {".gif", ".jpg", ".jpeg", ".png", ".svg"}; + private static final int MIN_WIDTH = 300; + private static final int MIN_HEIGHT = 200; + private static final String DEFAULT_IMAGE_FILE_PATH = "src/test/resources/images/default.jpg"; + + private String imageFilePath; + private File imageFile; + + private BufferedImage courseCoverImageData; + + public CourseCoverImage(String imageFilePath) { + validateImage(imageFilePath); + this.imageFilePath = imageFilePath; + } + + private void validateImage(String imageFilePath) { + if (imageFilePath == null) { + imageFilePath = DEFAULT_IMAGE_FILE_PATH; + } + + if (Arrays.stream(SUPPORTED_IMAGE_EXTENSIONS).noneMatch(imageFilePath::endsWith)) { + throw new IllegalArgumentException("지원하지 않는 이미지 형식입니다. (지원 형식: gif, jpg, jpeg, png, svg)"); + } + + try { + imageFile = new File(imageFilePath); + if (!imageFile.exists()) { + throw new IllegalArgumentException("강의 커버 이미지를 읽을 수 없습니다."); + } + } catch (Exception e) { + throw new IllegalArgumentException("강의 커버 이미지를 읽을 수 없습니다."); + } + + try { + courseCoverImageData = ImageIO.read(imageFile); + if (courseCoverImageData == null) { + throw new IllegalArgumentException("강의 커버 이미지를 읽을 수 없습니다."); + } + } catch (Exception e) { + throw new IllegalArgumentException("강의 커버 이미지를 읽을 수 없습니다."); + } + + if (courseCoverImageData.getWidth() < MIN_WIDTH || courseCoverImageData.getHeight() < MIN_HEIGHT) { + throw new IllegalArgumentException("강의 커버 이미지는 최소 300x200 픽셀 이상이어야 합니다."); + } + } +} diff --git a/src/main/java/nextstep/courses/domain/CourseFree.java b/src/main/java/nextstep/courses/domain/CourseFree.java new file mode 100644 index 0000000000..2d4fcdf786 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/CourseFree.java @@ -0,0 +1,20 @@ +package nextstep.courses.domain; + +import java.time.LocalDateTime; + +public class CourseFree extends Course { + + public static final Long MAX_ATTENDEES = Long.MAX_VALUE; + + public CourseFree(String title, Long creatorId, String courseCoverImageFilePath) { + super(title, creatorId, courseCoverImageFilePath, MAX_ATTENDEES); + } + + public CourseFree(Long id, String title, Long creatorId, LocalDateTime createdAt, LocalDateTime updatedAt, String courseCoverImageFilePath) { + super(id, title, creatorId, createdAt, updatedAt, courseCoverImageFilePath, MAX_ATTENDEES, CourseStatus.PREPARING); + } + + public CourseFree(Long id, String title, Long creatorId, LocalDateTime createdAt, LocalDateTime updatedAt, String courseCoverImageFilePath, CourseStatus courseStatus) { + super(id, title, creatorId, createdAt, updatedAt, courseCoverImageFilePath, MAX_ATTENDEES, courseStatus); + } +} diff --git a/src/main/java/nextstep/courses/domain/CoursePaid.java b/src/main/java/nextstep/courses/domain/CoursePaid.java new file mode 100644 index 0000000000..da6461089b --- /dev/null +++ b/src/main/java/nextstep/courses/domain/CoursePaid.java @@ -0,0 +1,19 @@ +package nextstep.courses.domain; + +import java.time.LocalDateTime; + +public class CoursePaid extends Course { + + public CoursePaid(String title, Long creatorId, String courseCoverImageFilePath, Long maxAttendees) { + super(title, creatorId, courseCoverImageFilePath, maxAttendees); + } + + public CoursePaid(Long id, String title, Long creatorId, LocalDateTime createdAt, LocalDateTime updatedAt, String courseCoverImageFilePath, Long maxAttendees) { + super(id, title, creatorId, createdAt, updatedAt, courseCoverImageFilePath, maxAttendees, CourseStatus.PREPARING); + } + + public CoursePaid(Long id, String title, Long creatorId, LocalDateTime createdAt, LocalDateTime updatedAt, String courseCoverImageFilePath, Long maxAttendees, CourseStatus courseStatus) { + super(id, title, creatorId, createdAt, updatedAt, courseCoverImageFilePath, maxAttendees, courseStatus); + } + +} diff --git a/src/main/java/nextstep/courses/domain/CourseStatus.java b/src/main/java/nextstep/courses/domain/CourseStatus.java new file mode 100644 index 0000000000..6977655c8f --- /dev/null +++ b/src/main/java/nextstep/courses/domain/CourseStatus.java @@ -0,0 +1,7 @@ +package nextstep.courses.domain; + +public enum CourseStatus { + PREPARING, + RECRUITING, + ENDED +} diff --git a/src/main/java/nextstep/courses/domain/Session.java b/src/main/java/nextstep/courses/domain/Session.java new file mode 100644 index 0000000000..bb9db13a65 --- /dev/null +++ b/src/main/java/nextstep/courses/domain/Session.java @@ -0,0 +1,5 @@ +package nextstep.courses.domain; + +public class Session { + +} diff --git a/src/main/java/nextstep/courses/domain/SessionList.java b/src/main/java/nextstep/courses/domain/SessionList.java new file mode 100644 index 0000000000..0a100317af --- /dev/null +++ b/src/main/java/nextstep/courses/domain/SessionList.java @@ -0,0 +1,5 @@ +package nextstep.courses.domain; + +public class SessionList { + +} diff --git a/src/main/java/nextstep/courses/infrastructure/JdbcCourseRepository.java b/src/main/java/nextstep/courses/infrastructure/JdbcCourseRepository.java index f9122cbe33..2e15d567b0 100644 --- a/src/main/java/nextstep/courses/infrastructure/JdbcCourseRepository.java +++ b/src/main/java/nextstep/courses/infrastructure/JdbcCourseRepository.java @@ -31,7 +31,11 @@ public Course findById(Long id) { rs.getString(2), rs.getLong(3), toLocalDateTime(rs.getTimestamp(4)), - toLocalDateTime(rs.getTimestamp(5))); + toLocalDateTime(rs.getTimestamp(5)), + null, // courseCoverImageFilePath + null, // maxAttendees + null // courseStatus + ); return jdbcTemplate.queryForObject(sql, rowMapper, id); } diff --git a/src/main/java/nextstep/qna/domain/AnswerList.java b/src/main/java/nextstep/qna/domain/AnswerList.java new file mode 100644 index 0000000000..9a85e7b281 --- /dev/null +++ b/src/main/java/nextstep/qna/domain/AnswerList.java @@ -0,0 +1,31 @@ +package nextstep.qna.domain; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class AnswerList implements Iterable { + + private final List answers; + + public AnswerList() { + this.answers = new ArrayList<>(); + } + + public AnswerList(List answers) { + this.answers = answers; + } + + public List getAnswers() { + return answers; + } + + public void add(Answer answer) { + answers.add(answer); + } + + @Override + public Iterator iterator() { + return answers.iterator(); + } +} diff --git a/src/main/java/nextstep/qna/domain/Question.java b/src/main/java/nextstep/qna/domain/Question.java index a448b2b0a5..248ecc5fee 100644 --- a/src/main/java/nextstep/qna/domain/Question.java +++ b/src/main/java/nextstep/qna/domain/Question.java @@ -16,7 +16,8 @@ public class Question { private NsUser writer; - private List answers = new ArrayList<>(); + // private List answers = new ArrayList<>(); + private AnswerList answers = new AnswerList(); private boolean deleted = false; @@ -96,7 +97,7 @@ public List delete(NsUser loginUser) throws CannotDeleteException } public List getAnswers() { - return answers; + return answers.getAnswers(); } @Override diff --git a/src/main/java/nextstep/users/domain/NsUser.java b/src/main/java/nextstep/users/domain/NsUser.java index 62ec5138cd..0e209b710b 100755 --- a/src/main/java/nextstep/users/domain/NsUser.java +++ b/src/main/java/nextstep/users/domain/NsUser.java @@ -1,10 +1,14 @@ package nextstep.users.domain; -import nextstep.qna.UnAuthorizedException; - import java.time.LocalDateTime; import java.util.Objects; +import nextstep.courses.domain.Course; +import nextstep.courses.domain.CourseFree; +import nextstep.courses.domain.CoursePaid; +import nextstep.courses.domain.CourseStatus; +import nextstep.qna.UnAuthorizedException; + public class NsUser { public static final GuestNsUser GUEST_USER = new GuestNsUser(); @@ -124,6 +128,22 @@ public boolean isGuestUser() { } } + public void registerCourse(Course course) { + if (course.getCourseStatus() == CourseStatus.PREPARING) { + throw new IllegalArgumentException("강의가 준비중입니다."); + } + if (course.getAttendees().size() >= course.getMaxAttendees()) { + throw new IllegalArgumentException("강의 수강 인원이 초과되었습니다."); + } + if (course instanceof CoursePaid) { + // Payment... + course.getAttendees().add(this); + } + if (course instanceof CourseFree) { + course.getAttendees().add(this); + } + } + @Override public String toString() { return "NsUser{" + diff --git a/src/test/java/nextstep/courses/domain/CourseCoverImageTest.java b/src/test/java/nextstep/courses/domain/CourseCoverImageTest.java new file mode 100644 index 0000000000..1afeb79263 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/CourseCoverImageTest.java @@ -0,0 +1,97 @@ +package nextstep.courses.domain; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +class CourseCoverImageTest { + + private static final String TEST_RESOURCES_PATH = "src/test/resources/images"; + private File validImageFile; + private File smallImageFile; + private File invalidImageFile; + + @BeforeEach + void setUp() throws IOException { + // Create test resources directory if it doesn't exist + File testDir = new File(TEST_RESOURCES_PATH); + if (!testDir.exists()) { + testDir.mkdirs(); + } + + // Create valid image (400x300) + validImageFile = createTestImage(400, 300, "valid.jpg"); + + // Create small image (200x150) + smallImageFile = createTestImage(200, 150, "small.jpg"); + + // Create invalid image file (not an image) + invalidImageFile = new File(TEST_RESOURCES_PATH, "invalid.jpg"); + invalidImageFile.createNewFile(); + } + + @Test + @DisplayName("유효한 이미지 파일로 CourseCoverImage를 생성할 수 있다") + void createCourseCoverImageWithValidImage() { + // when + CourseCoverImage courseCoverImage = new CourseCoverImage(validImageFile.getAbsolutePath()); + + // then + assertNotNull(courseCoverImage); + } + + @Test + @DisplayName("지원하지 않는 이미지 확장자면 예외가 발생한다") + void throwExceptionWhenImageExtensionIsNotSupported() { + // when & then + assertThatThrownBy(() -> { + new CourseCoverImage("invalid.txt"); + }).isInstanceOf(IllegalArgumentException.class) + .hasMessage("지원하지 않는 이미지 형식입니다. (지원 형식: gif, jpg, jpeg, png, svg)"); + } + + @Test + @DisplayName("이미지 파일이 존재하지 않으면 예외가 발생한다") + void throwExceptionWhenImageFileDoesNotExist() { + // when & then + assertThatThrownBy(() -> { + new CourseCoverImage("nonexistent.jpg"); + }).isInstanceOf(IllegalArgumentException.class) + .hasMessage("강의 커버 이미지를 읽을 수 없습니다."); + } + + @Test + @DisplayName("이미지 크기가 최소 크기보다 작으면 예외가 발생한다") + void throwExceptionWhenImageSizeIsTooSmall() { + // when & then + assertThatThrownBy(() -> { + new CourseCoverImage(smallImageFile.getAbsolutePath()); + }).isInstanceOf(IllegalArgumentException.class) + .hasMessage("강의 커버 이미지는 최소 300x200 픽셀 이상이어야 합니다."); + } + + @Test + @DisplayName("유효하지 않은 이미지 파일이면 예외가 발생한다") + void throwExceptionWhenImageFileIsInvalid() { + // when & then + assertThatThrownBy(() -> { + new CourseCoverImage(invalidImageFile.getAbsolutePath()); + }).isInstanceOf(IllegalArgumentException.class) + .hasMessage("강의 커버 이미지를 읽을 수 없습니다."); + } + + private File createTestImage(int width, int height, String fileName) throws IOException { + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + File imageFile = new File(TEST_RESOURCES_PATH, fileName); + ImageIO.write(image, "jpg", imageFile); + return imageFile; + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/courses/domain/CourseFreeTest.java b/src/test/java/nextstep/courses/domain/CourseFreeTest.java new file mode 100644 index 0000000000..d91567fe23 --- /dev/null +++ b/src/test/java/nextstep/courses/domain/CourseFreeTest.java @@ -0,0 +1,60 @@ +package nextstep.courses.domain; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +class CourseFreeTest { + + private static final String TEST_TITLE = "자바 기초 강의"; + private static final Long TEST_CREATOR_ID = 1L; + private static final String TEST_RESOURCES_PATH = "src/test/resources/images"; + private static final String TEST_COVER_IMAGE_PATH = TEST_RESOURCES_PATH + "/valid.jpg"; + + private CourseFree courseFree; + + @BeforeEach + void setUp() throws IOException { + // Create test resources directory if it doesn't exist + File testDir = new File(TEST_RESOURCES_PATH); + if (!testDir.exists()) { + testDir.mkdirs(); + } + + // Create valid image (400x300) + createTestImage(400, 300, "valid.jpg"); + + courseFree = new CourseFree(TEST_TITLE, TEST_CREATOR_ID, TEST_COVER_IMAGE_PATH); + } + + @Test + @DisplayName("무료 강의를 생성 테스트") + void createCourseFree() { + // then + assertThat(courseFree).isNotNull(); + assertThat(courseFree.getTitle()).isEqualTo(TEST_TITLE); + assertThat(courseFree.getCreatorId()).isEqualTo(TEST_CREATOR_ID); + } + + @Test + @DisplayName("무료 강의는 최대 수강 인원 제한이 없음") + void courseFreeHasNoMaxAttendeesLimit() { + // then + assertThat(courseFree.getMaxAttendees()).isEqualTo(Long.MAX_VALUE); + } + + private File createTestImage(int width, int height, String fileName) throws IOException { + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + File imageFile = new File(TEST_RESOURCES_PATH, fileName); + ImageIO.write(image, "jpg", imageFile); + return imageFile; + } +} \ No newline at end of file diff --git a/src/test/java/nextstep/courses/infrastructure/CourseRepositoryTest.java b/src/test/java/nextstep/courses/infrastructure/CourseRepositoryTest.java index f087fc0ad2..d2619c5709 100644 --- a/src/test/java/nextstep/courses/infrastructure/CourseRepositoryTest.java +++ b/src/test/java/nextstep/courses/infrastructure/CourseRepositoryTest.java @@ -28,7 +28,7 @@ void setUp() { @Test void crud() { - Course course = new Course("TDD, 클린 코드 with Java", 1L); + Course course = new Course("TDD, 클린 코드 with Java", 1L, null, null); int count = courseRepository.save(course); assertThat(count).isEqualTo(1); Course savedCourse = courseRepository.findById(1L); diff --git a/src/test/java/nextstep/qna/domain/AnswerTest.java b/src/test/java/nextstep/qna/domain/AnswerTest.java index 49029f497b..bd540006d9 100644 --- a/src/test/java/nextstep/qna/domain/AnswerTest.java +++ b/src/test/java/nextstep/qna/domain/AnswerTest.java @@ -23,7 +23,7 @@ public class AnswerTest { } @Test - @DisplayName("답변 삭제 실패") + @DisplayName("권한이 없는 경우 답변 삭제 실패") public void delete_실패() throws CannotDeleteException { assertThatThrownBy(() -> A2.delete(NsUserTest.JAVAJIGI)) .isInstanceOf(CannotDeleteException.class) diff --git a/src/test/java/nextstep/qna/domain/QuestionTest.java b/src/test/java/nextstep/qna/domain/QuestionTest.java index 958271ae39..393d301f0a 100644 --- a/src/test/java/nextstep/qna/domain/QuestionTest.java +++ b/src/test/java/nextstep/qna/domain/QuestionTest.java @@ -25,7 +25,7 @@ public class QuestionTest { } @Test - @DisplayName("질문 삭제 실패") + @DisplayName("권한이 없는 경우 질문 삭제 실패") public void delete_실패() throws CannotDeleteException { assertThatThrownBy(() -> Q2.delete(NsUserTest.JAVAJIGI)) .isInstanceOf(CannotDeleteException.class) diff --git a/src/test/resources/images/default.jpg b/src/test/resources/images/default.jpg new file mode 100644 index 0000000000..42e842efa2 Binary files /dev/null and b/src/test/resources/images/default.jpg differ diff --git a/src/test/resources/images/invalid.jpg b/src/test/resources/images/invalid.jpg new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/test/resources/images/invalid.txt b/src/test/resources/images/invalid.txt new file mode 100644 index 0000000000..e466dcbd8e --- /dev/null +++ b/src/test/resources/images/invalid.txt @@ -0,0 +1 @@ +invalid \ No newline at end of file diff --git a/src/test/resources/images/small.jpg b/src/test/resources/images/small.jpg new file mode 100644 index 0000000000..75dc009b78 Binary files /dev/null and b/src/test/resources/images/small.jpg differ diff --git a/src/test/resources/images/valid.jpg b/src/test/resources/images/valid.jpg new file mode 100644 index 0000000000..42e842efa2 Binary files /dev/null and b/src/test/resources/images/valid.jpg differ