Merge branch 'master' into master-web

# Conflicts:
#	src/main/java/com/mangkyu/employment/interview/config/web/WebMvcConfig.java
This commit is contained in:
MangKyu
2022-01-09 00:12:54 +09:00
17 changed files with 578 additions and 4 deletions

View File

@@ -39,6 +39,9 @@ dependencies {
// https://mvnrepository.com/artifact/org.apache.commons/commons-lang3
implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0'
// https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload
implementation group: 'commons-fileupload', name: 'commons-fileupload', version: '1.4'
implementation group: 'com.querydsl', name: 'querydsl-apt', version: '5.0.0'
implementation group: 'com.querydsl', name: 'querydsl-jpa', version: '5.0.0'

View File

@@ -23,6 +23,13 @@ public class AnswerController {
.build();
}
@PostMapping("/answer")
public ResponseEntity<Void> postAnswer(@RequestBody @Valid final AddAnswerRequest addAnswerRequest) throws QuizException {
answerService.addAnswer(addAnswerRequest);
return ResponseEntity.noContent()
.build();
}
@GetMapping("/answer/{resourceId}")
public ResponseEntity<GetAnswerResponse> getAnswer(@PathVariable final String resourceId) throws QuizException {
return ResponseEntity.ok(answerService.getAnswer(resourceId));

View File

@@ -0,0 +1,10 @@
package com.mangkyu.employment.interview.app.file.constants;
import lombok.NoArgsConstructor;
@NoArgsConstructor
public final class FileConstants {
public static final String FILE_API_PREFIX = "/file";
}

View File

@@ -0,0 +1,30 @@
package com.mangkyu.employment.interview.app.file.controller;
import com.mangkyu.employment.interview.app.file.constants.FileConstants;
import com.mangkyu.employment.interview.app.file.dto.FileUploadResponse;
import com.mangkyu.employment.interview.app.file.service.FileService;
import lombok.RequiredArgsConstructor;
import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import static com.mangkyu.employment.interview.app.file.constants.FileConstants.*;
@RestController
@RequiredArgsConstructor
public class FileController {
private final FileService fileService;
@PostMapping(FILE_API_PREFIX)
public ResponseEntity<FileUploadResponse> uploadFile(@RequestParam("upload") MultipartFile file) {
return ResponseEntity.ok(fileService.upload(file));
}
@GetMapping(FILE_API_PREFIX + "/{resourceId}")
public ResponseEntity<Resource> getFile(@PathVariable String resourceId) {
return ResponseEntity.ok(fileService.getFileAsResource(resourceId));
}
}

View File

@@ -0,0 +1,28 @@
package com.mangkyu.employment.interview.app.file.dto;
import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@Builder
@RequiredArgsConstructor
public class FileUploadResponse {
private final boolean uploaded;
private final String url;
public static FileUploadResponse uploadSuccessResponse(final String url) {
return FileUploadResponse.builder()
.uploaded(true)
.url(url)
.build();
}
public static FileUploadResponse uploadFailResponse() {
return FileUploadResponse.builder()
.uploaded(false)
.build();
}
}

View File

@@ -0,0 +1,27 @@
package com.mangkyu.employment.interview.app.file.entity;
import com.mangkyu.employment.interview.app.common.entity.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "my_file")
@Getter
@Builder
@NoArgsConstructor(force = true)
@AllArgsConstructor
public class MyFile extends BaseEntity {
@Column(nullable = false)
private String resourceId;
@Column(nullable = false)
private String fileName;
}

View File

@@ -0,0 +1,12 @@
package com.mangkyu.employment.interview.app.file.repository;
import com.mangkyu.employment.interview.app.file.entity.MyFile;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface FileRepository extends JpaRepository<MyFile, Long> {
Optional<MyFile> findByResourceId(final String resourceId);
}

View File

@@ -0,0 +1,90 @@
package com.mangkyu.employment.interview.app.file.service;
import com.mangkyu.employment.interview.app.common.erros.errorcode.CommonErrorCode;
import com.mangkyu.employment.interview.app.common.erros.exception.QuizException;
import com.mangkyu.employment.interview.app.file.dto.FileUploadResponse;
import com.mangkyu.employment.interview.app.file.entity.MyFile;
import com.mangkyu.employment.interview.app.file.repository.FileRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.PostConstruct;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
import static com.mangkyu.employment.interview.app.file.constants.FileConstants.FILE_API_PREFIX;
@Service
@RequiredArgsConstructor
@Slf4j
public class FileService {
private final FileRepository fileRepository;
@Value("${file.directory}")
private String fileDirectory;
@PostConstruct
public void init() throws IOException {
if (StringUtils.isBlank(fileDirectory)) {
throw new QuizException(CommonErrorCode.INTERNAL_SERVER_ERROR);
}
final Path fileDirectoryPath = Paths.get(fileDirectory);
if (Files.notExists(fileDirectoryPath)) {
Files.createDirectory(fileDirectoryPath);
}
}
public FileUploadResponse upload(final MultipartFile multipartFile) {
try {
final String resourceId = UUID.randomUUID().toString();
final String fileName = generateFileName(multipartFile.getOriginalFilename(), resourceId);
final File file = new File(fileDirectory + fileName);
FileUtils.copyInputStreamToFile(multipartFile.getInputStream(), file);
final MyFile myFile = MyFile.builder()
.resourceId(resourceId)
.fileName(fileName)
.build();
fileRepository.save(myFile);
return FileUploadResponse.uploadSuccessResponse(FILE_API_PREFIX + "/" + resourceId);
} catch (final IOException e) {
log.error("File Upload Fail", e);
return FileUploadResponse.uploadFailResponse();
}
}
private String generateFileName(final String originalFileName, final String uuid) {
final String fileExtension = FilenameUtils.getExtension(originalFileName);
return uuid + FilenameUtils.EXTENSION_SEPARATOR + fileExtension;
}
public Resource getFileAsResource(final String resourceId) {
final MyFile myFile = fileRepository.findByResourceId(resourceId)
.orElseThrow(() -> new QuizException(CommonErrorCode.RESOURCE_NOT_FOUND));
try {
final Path path = Paths.get(fileDirectory + myFile.getFileName());
return new ByteArrayResource(Files.readAllBytes(path));
} catch (final IOException e) {
log.error("getFileAsResource Fail", e);
throw new QuizException(CommonErrorCode.INTERNAL_SERVER_ERROR);
}
}
}

View File

@@ -62,7 +62,7 @@ public final class QuizDtoConverter {
private static String getAnswerResourceId(final Quiz quiz) {
return (quiz.getAnswer() == null)
? StringUtils.EMPTY
? null
: quiz.getAnswer().getResourceId();
}

View File

@@ -7,10 +7,18 @@ import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.ResourceUrlEncodingFilter;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(final CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE");
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/static/", "classpath:/public/", "classpath:/",
"classpath:/resources/", "classpath:/META-INF/resources/", "classpath:/META-INF/resources/webjars/" };

View File

@@ -1,3 +1,6 @@
# Custom
file.directory=/c/Users/Mang/IdeaProjects/InterviewSubscription/images/
# Datasource
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
spring.datasource.url=jdbc:mariadb://localhost/quiz?characterEncoding=utf-8

View File

@@ -1,3 +1,6 @@
# Custom
file.directory=/Users/user/IdeaProjects/interview/images/
# Datasource
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1

View File

@@ -85,7 +85,7 @@ class AnswerControllerTest {
// when
final ResultActions result = mockMvc.perform(
MockMvcRequestBuilders.put(url)
MockMvcRequestBuilders.post(url)
.content(new Gson().toJson(addAnswerRequest))
.contentType(MediaType.APPLICATION_JSON)
);
@@ -105,6 +105,50 @@ class AnswerControllerTest {
.build();
// when
final ResultActions result = mockMvc.perform(
MockMvcRequestBuilders.post(url)
.content(new Gson().toJson(addAnswerRequest))
.contentType(MediaType.APPLICATION_JSON)
);
// then
result.andExpect(status().isNoContent());
}
@ParameterizedTest
@MethodSource("provideParameters")
public void putAnswerFail_InvalidParameter(final String quizResourceId, final String desc) throws Exception {
// given
final String url = "/answer";
final AddAnswerRequest addAnswerRequest = AddAnswerRequest.builder()
.quizResourceId(quizResourceId)
.description(desc)
.build();
// when
final ResultActions result = mockMvc.perform(
MockMvcRequestBuilders.put(url)
.content(new Gson().toJson(addAnswerRequest))
.contentType(MediaType.APPLICATION_JSON)
);
// then
result.andExpect(status().isBadRequest());
}
@Test
public void putAnswerSuccess() throws Exception {
// given
final String url = "/answer";
final AddAnswerRequest addAnswerRequest = AddAnswerRequest.builder()
.quizResourceId(UUID.randomUUID().toString())
.description("desc")
.build();
// when
final ResultActions result = mockMvc.perform(
MockMvcRequestBuilders.put(url)

View File

@@ -0,0 +1,86 @@
package com.mangkyu.employment.interview.app.file.controller;
import com.google.gson.Gson;
import com.mangkyu.employment.interview.app.file.dto.FileUploadResponse;
import com.mangkyu.employment.interview.app.file.service.FileService;
import org.junit.jupiter.api.BeforeEach;
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.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import static com.mangkyu.employment.interview.app.file.constants.FileConstants.FILE_API_PREFIX;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.doReturn;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ExtendWith(MockitoExtension.class)
class FileControllerTest {
@InjectMocks
private FileController target;
@Mock
private FileService fileService;
private MockMvc mockMvc;
@BeforeEach
public void init() {
mockMvc = MockMvcBuilders.standaloneSetup(target).build();
}
@Test
public void uploadFileSuccess() throws Exception {
// given
final MockMultipartFile multipartFile = new MockMultipartFile("upload", new byte[0]);
final FileUploadResponse fileUploadResponse = FileUploadResponse.builder()
.uploaded(false)
.build();
doReturn(fileUploadResponse).when(fileService).upload(multipartFile);
// when
final ResultActions result = mockMvc.perform(
MockMvcRequestBuilders.multipart(FILE_API_PREFIX)
.file(multipartFile)
);
// then
final ResultActions resultActions = result.andExpect(status().isOk());
final String stringResponse = resultActions.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8);
final FileUploadResponse responseResult = new Gson().fromJson(stringResponse, FileUploadResponse.class);
assertThat(responseResult.isUploaded()).isFalse();
}
@Test
public void getResource() throws Exception {
// given
final String resourceId = UUID.randomUUID().toString();
final String url = FILE_API_PREFIX + "/" + resourceId;
final Resource resource = new ByteArrayResource(new byte[0]);
doReturn(resource).when(fileService).getFileAsResource(resourceId);
// when
final ResultActions result = mockMvc.perform(
MockMvcRequestBuilders.get(url)
);
// then
result.andExpect(status().isOk());
}
}

View File

@@ -0,0 +1,60 @@
package com.mangkyu.employment.interview.app.file.repository;
import com.mangkyu.employment.interview.JpaTestConfig;
import com.mangkyu.employment.interview.app.file.entity.MyFile;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Optional;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
@JpaTestConfig
class FileRepositoryTest {
@Autowired
private FileRepository fileRepository;
@Test
public void insertMyFile() {
// given
final String resourceId = UUID.randomUUID().toString();
final String fileName = "fileName";
final MyFile myFile = MyFile.builder()
.resourceId(resourceId)
.fileName(fileName)
.build();
// when
final MyFile result = fileRepository.save(myFile);
// then
assertThat(result.getResourceId()).isEqualTo(resourceId);
assertThat(result.getFileName()).isEqualTo(fileName);
}
@Test
public void findFileByResourceId() {
// given
final String resourceId = UUID.randomUUID().toString();
final String fileName = "fileName";
final MyFile myFile = MyFile.builder()
.resourceId(resourceId)
.fileName(fileName)
.build();
fileRepository.save(myFile);
// when
final Optional<MyFile> optionalResult = fileRepository.findByResourceId(resourceId);
// then
assertThat(optionalResult.isPresent()).isTrue();
final MyFile result = optionalResult.get();
assertThat(result.getResourceId()).isEqualTo(resourceId);
assertThat(result.getFileName()).isEqualTo(fileName);
}
}

View File

@@ -0,0 +1,163 @@
package com.mangkyu.employment.interview.app.file.service;
import com.mangkyu.employment.interview.app.common.erros.errorcode.CommonErrorCode;
import com.mangkyu.employment.interview.app.common.erros.exception.QuizException;
import com.mangkyu.employment.interview.app.file.dto.FileUploadResponse;
import com.mangkyu.employment.interview.app.file.entity.MyFile;
import com.mangkyu.employment.interview.app.file.repository.FileRepository;
import org.apache.commons.io.FileUtils;
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.core.io.Resource;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class FileServiceTest {
@InjectMocks
private FileService target;
@Mock
private FileRepository fileRepository;
@Test
public void initFail_fileDirectoryEmpty() {
// given
// when
final QuizException result = assertThrows(QuizException.class, () -> target.init());
// then
assertThat(result.getErrorCode()).isEqualTo(CommonErrorCode.INTERNAL_SERVER_ERROR);
}
@Test
public void initSuccess_fileDirectoryExists() throws IOException {
// given
ReflectionTestUtils.setField(target, "fileDirectory", "/");
// when
target.init();
// then
}
@Test
public void initSuccess_createFileDirectory() throws IOException {
// given
final Path currentPath = Paths.get("");
final String testDirectory = currentPath.toAbsolutePath() + "/temp" + UUID.randomUUID() + "/";
FileUtils.deleteDirectory(new File(testDirectory));
ReflectionTestUtils.setField(target, "fileDirectory", testDirectory);
// when
target.init();
// then
FileUtils.deleteDirectory(new File(testDirectory));
}
@Test
public void fileUploadFail_IOException() throws IOException {
// given
final Path currentPath = Paths.get("");
final String testDirectory = currentPath.toAbsolutePath() + "/temp" + UUID.randomUUID() + "/";
FileUtils.deleteDirectory(new File(testDirectory));
ReflectionTestUtils.setField(target, "fileDirectory", testDirectory);
final MultipartFile multipartFile = mock(MultipartFile.class);
doThrow(new IOException()).when(multipartFile).getInputStream();
// when
final FileUploadResponse result = target.upload(multipartFile);
// then
assertThat(result.isUploaded()).isFalse();
// then
FileUtils.deleteDirectory(new File(testDirectory));
}
@Test
public void fileUploadSuccess() throws IOException {
// given
final Path currentPath = Paths.get("");
final String fileName = "fileName";
final MultipartFile multipartFile = new MockMultipartFile(fileName, new byte[]{});
final String testDirectory = currentPath.toAbsolutePath() + "/temp" + UUID.randomUUID() + "/";
ReflectionTestUtils.setField(target, "fileDirectory", testDirectory);
FileUtils.deleteDirectory(new File(testDirectory));
// when
final FileUploadResponse result = target.upload(multipartFile);
// then
assertThat(result.isUploaded()).isTrue();
assertThat(result.getUrl()).isNotNull();
// then
FileUtils.deleteDirectory(new File(testDirectory));
}
@Test
public void getFileAsResourceFail_MyFileNotExists() {
// given
final String resourceId = UUID.randomUUID().toString();
doReturn(Optional.empty()).when(fileRepository).findByResourceId(resourceId);
// when
final QuizException result = assertThrows(QuizException.class, () -> target.getFileAsResource(resourceId));
// then
assertThat(result.getErrorCode()).isEqualTo(CommonErrorCode.RESOURCE_NOT_FOUND);
}
@Test
public void getFileAsResourceFail_IOException() throws IOException {
// given
final String resourceId = UUID.randomUUID().toString();
final Path currentPath = Paths.get("");
final MultipartFile multipartFile = new MockMultipartFile(resourceId, new byte[]{});
final String testDirectory = currentPath.toAbsolutePath() + "/temp" + resourceId + "/";
FileUtils.deleteDirectory(new File(testDirectory));
ReflectionTestUtils.setField(target, "fileDirectory", testDirectory);
final File file = new File(testDirectory + resourceId);
FileUtils.copyInputStreamToFile(multipartFile.getInputStream(), file);
final MyFile myFile = MyFile.builder()
.resourceId(resourceId)
.fileName(resourceId)
.build();
doReturn(Optional.of(myFile)).when(fileRepository).findByResourceId(resourceId);
// when
final Resource result = target.getFileAsResource(resourceId);
// then
assertThat(result).isNotNull();
FileUtils.deleteDirectory(new File(testDirectory));
}
}

View File

@@ -84,7 +84,7 @@ class QuizDtoConverterTest {
assertThat(result.getQuizLevelList().size()).isEqualTo(quiz.getQuizLevel().size());
assertThat(result.getCreatedAt()).isEqualTo(Timestamp.valueOf(quiz.getCreatedAt()).getTime());
assertThat(result.getCategory().getCode()).isEqualTo(enumMapperValue(quiz.getQuizCategory()).getCode());
assertThat(result.getAnswerResourceId()).isEqualTo(StringUtils.EMPTY);
assertThat(result.getAnswerResourceId()).isNull();
}
@Test
@@ -120,7 +120,7 @@ class QuizDtoConverterTest {
assertThat(result.getQuizLevelList().size()).isEqualTo(quiz.getQuizLevel().size());
assertThat(result.getCreatedAt()).isEqualTo(Timestamp.valueOf(quiz.getCreatedAt()).getTime());
assertThat(result.getCategory()).isNull();
assertThat(result.getAnswerResourceId()).isEqualTo(StringUtils.EMPTY);
assertThat(result.getAnswerResourceId()).isNull();
}
private EnumMapperValue enumMapperValue(final EnumMapperType enumMapperType) {