diff --git a/mitube-app/build.gradle b/mitube-app/build.gradle index ba5b949..a089b92 100644 --- a/mitube-app/build.gradle +++ b/mitube-app/build.gradle @@ -14,6 +14,7 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' + testImplementation 'io.rest-assured:rest-assured:5.3.1' } tasks.named('bootBuildImage') { @@ -22,4 +23,4 @@ tasks.named('bootBuildImage') { tasks.named('test') { useJUnitPlatform() -} \ No newline at end of file +} diff --git a/mitube-app/src/main/java/com/misim/config/SecurityConfig.java b/mitube-app/src/main/java/com/misim/config/SecurityConfig.java index 79b5378..8d86497 100644 --- a/mitube-app/src/main/java/com/misim/config/SecurityConfig.java +++ b/mitube-app/src/main/java/com/misim/config/SecurityConfig.java @@ -24,7 +24,8 @@ public WebSecurityCustomizer webSecurityCustomizer() { "/comments/**", "/channels/**", "/home", - "/videofiles/**" + "/videofiles/**", + "/videoMetadata/**" ); } diff --git a/mitube-app/src/main/java/com/misim/controller/VideoMetadataController.java b/mitube-app/src/main/java/com/misim/controller/VideoMetadataController.java new file mode 100644 index 0000000..4427d46 --- /dev/null +++ b/mitube-app/src/main/java/com/misim/controller/VideoMetadataController.java @@ -0,0 +1,61 @@ +package com.misim.controller; + +import com.misim.controller.model.Response.MetadataResponse; +import com.misim.entity.VideoMetadata; +import com.misim.exception.CommonResponse; +import com.misim.service.VideoMetadataService; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/videoMetadata") +public class VideoMetadataController { + + private final VideoMetadataService videoMetadataService; + + public VideoMetadataController(VideoMetadataService videoMetadataService) { + this.videoMetadataService = videoMetadataService; + } + + @GetMapping("/{videoMetadataId}") + public CommonResponse getVideoMetadata(@PathVariable Long videoMetadataId) { + + VideoMetadata metadata = videoMetadataService.read(videoMetadataId); + + MetadataResponse response = new MetadataResponse(metadata.getViewCount(), metadata.getLikeCount(), metadata.getDislikeCount()); + + return CommonResponse + .builder() + .body(response) + .build(); + } + + @PostMapping("/{videoMetadataId}/view") + public void addVideoMetadataViewCount(@PathVariable Long videoMetadataId) { + + videoMetadataService.updateViewCount(videoMetadataId); + } + + @PostMapping("/{videoMetadataId}/like") + public void addVideoMetadataLikeCount(@PathVariable Long videoMetadataId, @RequestParam Boolean isChecked) { + + videoMetadataService.updateLikeCount(videoMetadataId, isChecked); + } + + @PostMapping("/{videoMetadataId}/dislike") + public void addVideoMetadataDislikeCount(@PathVariable Long videoMetadataId, @RequestParam Boolean isChecked) { + + videoMetadataService.updateDislikeCount(videoMetadataId, isChecked); + } + + @DeleteMapping("/{videoMetadataId}") + public void deleteVideoMetadata(@PathVariable Long videoMetadataId) { + + videoMetadataService.delete(videoMetadataId); + } +} diff --git a/mitube-app/src/main/java/com/misim/controller/model/Response/MetadataResponse.java b/mitube-app/src/main/java/com/misim/controller/model/Response/MetadataResponse.java new file mode 100644 index 0000000..e5c4639 --- /dev/null +++ b/mitube-app/src/main/java/com/misim/controller/model/Response/MetadataResponse.java @@ -0,0 +1,6 @@ +package com.misim.controller.model.Response; + +public record MetadataResponse ( + Long viewCount, Long likeCount, Long dislikeCount +) { +} diff --git a/mitube-app/src/main/java/com/misim/entity/VideoCatalog.java b/mitube-app/src/main/java/com/misim/entity/VideoCatalog.java index 2e8851b..306bb32 100644 --- a/mitube-app/src/main/java/com/misim/entity/VideoCatalog.java +++ b/mitube-app/src/main/java/com/misim/entity/VideoCatalog.java @@ -4,7 +4,7 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import lombok.Builder; import lombok.Getter; @@ -24,16 +24,20 @@ public class VideoCatalog extends BaseTimeEntity { private String description; - @ManyToOne + private Integer categoryId; + + @OneToOne private VideoFile videoFile; - private Integer categoryId; + @OneToOne + private VideoMetadata videoMetadata; @Builder - public VideoCatalog(String title, String description, VideoFile videoFile, Integer categoryId) { + public VideoCatalog(String title, String description, Integer categoryId, VideoFile videoFile, VideoMetadata videoMetadata) { this.title = title; this.description = description; - this.videoFile = videoFile; this.categoryId = categoryId; + this.videoFile = videoFile; + this.videoMetadata = videoMetadata; } } diff --git a/mitube-app/src/main/java/com/misim/entity/VideoMetadata.java b/mitube-app/src/main/java/com/misim/entity/VideoMetadata.java new file mode 100644 index 0000000..ceed3aa --- /dev/null +++ b/mitube-app/src/main/java/com/misim/entity/VideoMetadata.java @@ -0,0 +1,56 @@ +package com.misim.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor +public class VideoMetadata extends BaseTimeEntity{ + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Long viewCount; + + private Long likeCount; + + private Long dislikeCount; + + @Builder + public VideoMetadata(Long viewCount, Long likeCount, Long dislikeCount) { + this.viewCount = viewCount; + this.likeCount = likeCount; + this.dislikeCount = dislikeCount; + } + + public void incrementViewCount() { + this.viewCount++; + } + + public void incrementLikeCount() { + this.likeCount++; + } + + public void incrementDislikeCount() { + this.dislikeCount++; + } + + public void decrementLikeCount() { + if (this.likeCount > 0) { + this.likeCount--; + } + } + + public void decrementDislikeCount() { + if (this.dislikeCount > 0) { + this.dislikeCount--; + } + } +} diff --git a/mitube-app/src/main/java/com/misim/exception/GlobalExceptionHandler.java b/mitube-app/src/main/java/com/misim/exception/GlobalExceptionHandler.java index 1208455..4d4a2e6 100644 --- a/mitube-app/src/main/java/com/misim/exception/GlobalExceptionHandler.java +++ b/mitube-app/src/main/java/com/misim/exception/GlobalExceptionHandler.java @@ -1,10 +1,12 @@ package com.misim.exception; +import lombok.extern.slf4j.Slf4j; import org.springframework.core.annotation.Order; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +@Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @@ -12,7 +14,7 @@ public class GlobalExceptionHandler { @ExceptionHandler(MitubeException.class) public ResponseEntity> handleMitubeException(MitubeException e) { - e.fillInStackTrace(); + log.error("Handler MitubeException: {}", e.getMessage(), e); CommonResponse commonResponse = new CommonResponse<>(e.getErrorCode().getCode(), e.getErrorCode().getMessage(), null); @@ -23,7 +25,7 @@ public ResponseEntity> handleMitubeException(MitubeException e @ExceptionHandler(Exception.class) public ResponseEntity> handleUnknownException(Exception e) { - e.fillInStackTrace(); + log.error("Handler UnknownException: {}", e.getMessage(), e); CommonResponse commonResponse = new CommonResponse<>( MitubeErrorCode.UNKNOWN_EXCEPTION.getCode(), diff --git a/mitube-app/src/main/java/com/misim/repository/VideoMetadataRepository.java b/mitube-app/src/main/java/com/misim/repository/VideoMetadataRepository.java new file mode 100644 index 0000000..a41ec79 --- /dev/null +++ b/mitube-app/src/main/java/com/misim/repository/VideoMetadataRepository.java @@ -0,0 +1,10 @@ +package com.misim.repository; + +import com.misim.entity.VideoMetadata; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface VideoMetadataRepository extends JpaRepository { + +} diff --git a/mitube-app/src/main/java/com/misim/service/VideoMetadataService.java b/mitube-app/src/main/java/com/misim/service/VideoMetadataService.java new file mode 100644 index 0000000..f0b045d --- /dev/null +++ b/mitube-app/src/main/java/com/misim/service/VideoMetadataService.java @@ -0,0 +1,95 @@ +package com.misim.service; + +import com.misim.entity.VideoMetadata; +import com.misim.repository.VideoMetadataRepository; +import java.util.NoSuchElementException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class VideoMetadataService { + + private final VideoMetadataRepository videoMetadataRepository; + + public VideoMetadata create() { + + VideoMetadata metadata = VideoMetadata.builder() + .viewCount(0L) + .likeCount(0L) + .dislikeCount(0L) + .build(); + + return videoMetadataRepository.save(metadata); + } + + public VideoMetadata read(Long id) { + + return videoMetadataRepository.findById(id) + .orElseThrow(NoSuchElementException::new); + } + + public Long readViewCount(Long id) { + + return videoMetadataRepository.findById(id) + .orElseThrow(NoSuchElementException::new) + .getViewCount(); + } + + public Long readLikeCount(Long id) { + + return videoMetadataRepository.findById(id) + .orElseThrow(NoSuchElementException::new) + .getLikeCount(); + } + + public Long readDislikeCount(Long id) { + + return videoMetadataRepository.findById(id) + .orElseThrow(NoSuchElementException::new) + .getDislikeCount(); + } + + public void updateViewCount(Long id) { + + VideoMetadata metadata = videoMetadataRepository.findById(id) + .orElseThrow(NoSuchElementException::new); + + metadata.incrementViewCount(); + + videoMetadataRepository.save(metadata); + } + + public void updateLikeCount(Long id, boolean isChecked) { + + VideoMetadata metadata = videoMetadataRepository.findById(id) + .orElseThrow(NoSuchElementException::new); + + if (isChecked) { + metadata.incrementLikeCount(); + } else { + metadata.decrementLikeCount(); + } + + videoMetadataRepository.save(metadata); + } + + public void updateDislikeCount(Long id, boolean isChecked) { + + VideoMetadata metadata = videoMetadataRepository.findById(id) + .orElseThrow(NoSuchElementException::new); + + if (isChecked) { + metadata.incrementDislikeCount(); + } else { + metadata.decrementDislikeCount(); + } + + videoMetadataRepository.save(metadata); + } + + public void delete(Long id) { + + videoMetadataRepository.deleteById(id); + } +} diff --git a/mitube-app/src/test/java/com/misim/controller/VideoFileControllerTest.java b/mitube-app/src/test/java/com/misim/controller/VideoFileControllerTest.java index c25a0e2..7c801c0 100644 --- a/mitube-app/src/test/java/com/misim/controller/VideoFileControllerTest.java +++ b/mitube-app/src/test/java/com/misim/controller/VideoFileControllerTest.java @@ -74,7 +74,7 @@ void testStreamVideo_Success() throws Exception { // given Long videoId = 1L; VideoFile videoFile = new VideoFile("path/to/video.mp4"); - VideoCatalog videoCatalog = new VideoCatalog("title", "description", videoFile, 0); + VideoCatalog videoCatalog = new VideoCatalog("title", "description", 0, videoFile, null); when(videoService.getVideo(videoId)).thenReturn(videoCatalog); byte[] videoContent = "test video content".getBytes(); @@ -101,4 +101,4 @@ void testStreamVideo_VideoNotFound() throws Exception { mockMvc.perform(get("/videofiles/1")) .andExpect(status().isNotFound()); } -} \ No newline at end of file +} diff --git a/mitube-app/src/test/java/com/misim/controller/VideoMetadataControllerTest.java b/mitube-app/src/test/java/com/misim/controller/VideoMetadataControllerTest.java new file mode 100644 index 0000000..f15fd05 --- /dev/null +++ b/mitube-app/src/test/java/com/misim/controller/VideoMetadataControllerTest.java @@ -0,0 +1,155 @@ +package com.misim.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.misim.controller.model.Response.MetadataResponse; +import com.misim.entity.VideoMetadata; +import com.misim.exception.CommonResponse; +import com.misim.service.VideoMetadataService; +import io.restassured.RestAssured; +import io.restassured.common.mapper.TypeRef; +import io.restassured.response.ExtractableResponse; +import io.restassured.response.Response; +import java.util.NoSuchElementException; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +class VideoMetadataControllerTest { + + @Autowired + private VideoMetadataService videoMetadataService; + + @Test + void getVideoMetadata() { + + VideoMetadata metadata = videoMetadataService.create(); + + ExtractableResponse response = RestAssured + .given() + .log() + .all() + .pathParam("videoMetadataId", metadata.getId()) + .when() + .get("/videoMetadata/{videoMetadataId}") + .then() + .log() + .all() + .statusCode(200) + .extract(); + + CommonResponse commonResponse = response.as( + new TypeRef>() {}); + + assertThat(commonResponse.getBody().viewCount()).isEqualTo(metadata.getViewCount()); + assertThat(commonResponse.getBody().likeCount()).isEqualTo(metadata.getLikeCount()); + assertThat(commonResponse.getBody().dislikeCount()).isEqualTo(metadata.getDislikeCount()); + + } + + @Test + void addVideoMetadataViewCount() { + + VideoMetadata metadata1 = videoMetadataService.create(); + + RestAssured + .given() + .log() + .all() + .pathParam("videoMetadataId", metadata1.getId()) + .when() + .post("/videoMetadata/{videoMetadataId}/view") + .then() + .log() + .all() + .statusCode(200) + .extract(); + + VideoMetadata metadata2 = videoMetadataService.read(metadata1.getId()); + + assertThat(metadata2.getViewCount()).isGreaterThan(metadata1.getViewCount()); + assertThat(metadata2.getLikeCount()).isEqualTo(metadata1.getLikeCount()); + assertThat(metadata2.getDislikeCount()).isEqualTo(metadata1.getDislikeCount()); + + } + + @Test + void addVideoMetadataLikeCount() { + + VideoMetadata metadata1 = videoMetadataService.create(); + + RestAssured + .given() + .log() + .all() + .pathParam("videoMetadataId", metadata1.getId()) + .queryParam("isChecked", true) + .when() + .post("/videoMetadata/{videoMetadataId}/like") + .then() + .log() + .all() + .statusCode(200) + .extract(); + + VideoMetadata metadata2 = videoMetadataService.read(metadata1.getId()); + + assertThat(metadata2.getViewCount()).isEqualTo(metadata1.getViewCount()); + assertThat(metadata2.getLikeCount()).isGreaterThan(metadata1.getLikeCount()); + assertThat(metadata2.getDislikeCount()).isEqualTo(metadata1.getDislikeCount()); + } + + @Test + void addVideoMetadataDislikeCount() { + + VideoMetadata metadata1 = videoMetadataService.create(); + + RestAssured + .given() + .log() + .all() + .pathParam("videoMetadataId", metadata1.getId()) + .queryParam("isChecked", true) + .when() + .post("/videoMetadata/{videoMetadataId}/dislike") + .then() + .log() + .all() + .statusCode(200) + .extract(); + + VideoMetadata metadata2 = videoMetadataService.read(metadata1.getId()); + + assertThat(metadata2.getViewCount()).isEqualTo(metadata1.getViewCount()); + assertThat(metadata2.getLikeCount()).isEqualTo(metadata1.getLikeCount()); + assertThat(metadata2.getDislikeCount()).isGreaterThan(metadata1.getDislikeCount()); + + } + + @Test + void deleteVideoMetadata() { + + VideoMetadata metadata1 = videoMetadataService.create(); + + RestAssured + .given() + .log() + .all() + .pathParam("videoMetadataId", metadata1.getId()) + .when() + .delete("/videoMetadata/{videoMetadataId}") + .then() + .log() + .all() + .statusCode(200) + .extract(); + + assertThatThrownBy(() -> videoMetadataService.read(metadata1.getId())) + .isInstanceOf(NoSuchElementException.class); + + } +} diff --git a/mitube-app/src/test/java/com/misim/service/VideoMetadataServiceTest.java b/mitube-app/src/test/java/com/misim/service/VideoMetadataServiceTest.java new file mode 100644 index 0000000..ccc9e41 --- /dev/null +++ b/mitube-app/src/test/java/com/misim/service/VideoMetadataServiceTest.java @@ -0,0 +1,96 @@ +package com.misim.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.misim.entity.VideoMetadata; +import java.util.NoSuchElementException; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.ClassMode; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@DirtiesContext(classMode = ClassMode.BEFORE_EACH_TEST_METHOD) +class VideoMetadataServiceTest { + + @Autowired + private VideoMetadataService videoMetadataService; + + @Test + void create() { + + VideoMetadata metadata = videoMetadataService.create(); + + assertThat(metadata.getViewCount()).isEqualTo(0); + assertThat(metadata.getLikeCount()).isEqualTo(0); + assertThat(metadata.getDislikeCount()).isEqualTo(0); + + } + + @Test + void read() { + + VideoMetadata metadata1 = videoMetadataService.create(); + + VideoMetadata metadata2 = videoMetadataService.read(metadata1.getId()); + + assertThat(metadata1.getId()).isEqualTo(metadata2.getId()); + assertThat(metadata1.getViewCount()).isEqualTo(metadata2.getViewCount()); + assertThat(metadata1.getLikeCount()).isEqualTo(metadata2.getLikeCount()); + assertThat(metadata1.getDislikeCount()).isEqualTo(metadata2.getDislikeCount()); + + } + + @Test + void updateViewCount() { + + VideoMetadata metadata1 = videoMetadataService.create(); + + videoMetadataService.updateViewCount(metadata1.getId()); + + VideoMetadata metadata2 = videoMetadataService.read(metadata1.getId()); + + assertThat(metadata2.getViewCount()).isGreaterThan(metadata1.getViewCount()); + + } + + @Test + void updateLikeCount() { + + VideoMetadata metadata1 = videoMetadataService.create(); + + videoMetadataService.updateLikeCount(metadata1.getId(), true); + + VideoMetadata metadata2 = videoMetadataService.read(metadata1.getId()); + + assertThat(metadata2.getLikeCount()).isGreaterThan(metadata1.getLikeCount()); + + } + + @Test + void updateDislikeCount() { + + VideoMetadata metadata1 = videoMetadataService.create(); + + videoMetadataService.updateDislikeCount(metadata1.getId(), true); + + VideoMetadata metadata2 = videoMetadataService.read(metadata1.getId()); + + assertThat(metadata2.getDislikeCount()).isGreaterThan(metadata1.getDislikeCount()); + + } + + @Test + void delete() { + + VideoMetadata metadata1 = videoMetadataService.create(); + + videoMetadataService.delete(metadata1.getId()); + + assertThatThrownBy(() -> videoMetadataService.read(metadata1.getId())) + .isInstanceOf(NoSuchElementException.class); + + } +}