Skip to content

Commit

Permalink
✨ feat: add banned word when create item (#160)
Browse files Browse the repository at this point in the history
* ✨ feat(api): add banned word validator

* ✨ feat(api): add sql to flyway

* ✨ fix(api): add fix query

* ✨ feat(api) : add NotBannedWord annotation to ItemCreateRequestDto

* ♻️ chore: change contentWord to contentWords

* 🐛 fix: add access to noargsconstructor

* 🐛 fix: fix test case and if null banned word is true
  • Loading branch information
seonghun-dev committed Nov 8, 2023
1 parent 42316b4 commit 49e4134
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.depromeet.common.annotation;
import com.depromeet.common.annotation.validator.BannedWordValidator;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Target(FIELD)
@Retention(RUNTIME)
@Constraint(validatedBy = BannedWordValidator.class)
public @interface NotBannedWord {

String message() default "Cannot use banned word";

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.depromeet.common.annotation.validator;


import com.depromeet.common.annotation.NotBannedWord;
import com.depromeet.common.repository.BannedWordRepository;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import lombok.RequiredArgsConstructor;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
@RequiredArgsConstructor
public class BannedWordValidator implements ConstraintValidator<NotBannedWord, String> {

private final BannedWordRepository bannedWordRepository;
@Override
@Transactional(readOnly = true)
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return true;
}

var contentWords = List.of(value.split(" "));
var bannedWord = bannedWordRepository.findBannedWordsInWordList(contentWords);

if (!bannedWord.isEmpty()) {
context.buildConstraintViolationWithTemplate("Cannot use banned word : " + bannedWord.get(0))
.addConstraintViolation();
return false;
}

return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ public enum ErrorCode {

CAN_NOT_BLOCK_SELF(HttpStatus.BAD_REQUEST, "C-0016", "Can not block myself"),

SEARCH_TERM_NOT_FOUND(HttpStatus.NOT_FOUND, "C-0010", "Search Term Not Found");
SEARCH_TERM_NOT_FOUND(HttpStatus.NOT_FOUND, "C-0010", "Search Term Not Found"),

CANNOT_USE_BANNED_WORD(HttpStatus.BAD_REQUEST, "C-0010", "Cannot Use Banned Word");

private final HttpStatus status;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.depromeet.common.repository;

import com.depromeet.common.entity.BannedWord;
import io.lettuce.core.dynamic.annotation.Param;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface BannedWordRepository extends JpaRepository<BannedWord, Long> {

@Query("SELECT bw.word FROM BannedWord bw WHERE bw.word IN :words")
List<String> findBannedWordsInWordList(@Param("words") List<String> words);

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.depromeet.domains.item.dto.request;

import com.depromeet.common.annotation.NotBannedWord;
import com.depromeet.domains.music.dto.request.MusicRequestDto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
Expand All @@ -25,5 +26,6 @@ public class ItemCreateRequestDto {

@Schema(description = "콘텐츠", example = "블라블라")
@NotNull(message = "Content is required")
@NotBannedWord
private String content;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CREATE TABLE banned_word (
banned_word_id BIGINT AUTO_INCREMENT PRIMARY KEY,
word VARCHAR(20) NOT NULL
);

CREATE INDEX idx__banned_word_word ON banned_word (word);
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package unit.domains.item.controller;

import com.depromeet.domains.item.controller.ItemController;
import com.depromeet.common.annotation.validator.BannedWordValidator;
import com.depromeet.common.error.GlobalExceptionHandler;
import com.depromeet.common.repository.BannedWordRepository;
import com.depromeet.domains.item.controller.ItemController;
import com.depromeet.domains.item.dto.request.ItemCreateRequestDto;
import com.depromeet.domains.item.dto.request.ItemLocationRequestDto;
import com.depromeet.domains.item.dto.request.NearItemPointRequestDto;
Expand All @@ -24,6 +26,7 @@
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
Expand All @@ -41,7 +44,7 @@
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@ContextConfiguration(classes = ItemController.class)
@ContextConfiguration(classes = {ItemController.class, ValidationAutoConfiguration.class})
@WebMvcTest(controllers = {ItemController.class}, excludeAutoConfiguration = {SecurityAutoConfiguration.class})
@Import({ItemController.class, GlobalExceptionHandler.class})
@DisplayName("[API][Controller] ItemController 테스트")
Expand All @@ -56,6 +59,12 @@ public class ItemControllerTest {
@MockBean
ItemService itemService;

@MockBean
BannedWordRepository bannedWordRepository;

@MockBean
BannedWordValidator bannedWordValidator;

@MockBean
ItemLikeService itemLikeService;

Expand All @@ -76,8 +85,10 @@ void createItem_ValidRequest_ReturnsCreated() throws Exception {
ItemCreateRequestDto itemRequestDto = new ItemCreateRequestDto(itemLocationRequestDto, musicRequestDto, "블라블라");
ItemResponseDto itemResponseDto = createValidItemResponseDto();

given(bannedWordRepository.findBannedWordsInWordList(any())).willReturn(List.of());
given(itemService.create(mockUser, itemRequestDto)).willReturn(itemResponseDto);


var response = mvc.perform(
post("/items")
.contentType(MediaType.APPLICATION_JSON)
Expand All @@ -97,6 +108,7 @@ void createItem_InvalidMusicTitle_ReturnsBadRequest() throws Exception {
ItemCreateRequestDto itemRequestDto = new ItemCreateRequestDto(itemLocationRequestDto, musicRequestDto, "블라블라");
ItemResponseDto itemResponseDto = createValidItemResponseDto();

given(bannedWordRepository.findBannedWordsInWordList(any())).willReturn(List.of());
given(itemService.create(mockUser, itemRequestDto)).willReturn(itemResponseDto);

var response = mvc.perform(
Expand All @@ -116,6 +128,7 @@ void createItem_InvalidMusicArtist_ReturnsBadRequest() throws Exception {
ItemCreateRequestDto itemRequestDto = new ItemCreateRequestDto(itemLocationRequestDto, musicRequestDto, "블라블라");
ItemResponseDto itemResponseDto = createValidItemResponseDto();

given(bannedWordRepository.findBannedWordsInWordList(any())).willReturn(List.of());
given(itemService.create(mockUser, itemRequestDto)).willReturn(itemResponseDto);

var response = mvc.perform(
Expand All @@ -135,6 +148,7 @@ void createItem_InvalidMusicAlbum_ReturnsBadRequest() throws Exception {
ItemCreateRequestDto itemRequestDto = new ItemCreateRequestDto(itemLocationRequestDto, musicRequestDto, "블라블라");
ItemResponseDto itemResponseDto = createValidItemResponseDto();

given(bannedWordRepository.findBannedWordsInWordList(any())).willReturn(List.of());
given(itemService.create(mockUser, itemRequestDto)).willReturn(itemResponseDto);

var response = mvc.perform(
Expand All @@ -154,6 +168,7 @@ void createItem_InvalidAlbumCover_ReturnsBadRequest() throws Exception {
ItemCreateRequestDto itemRequestDto = new ItemCreateRequestDto(itemLocationRequestDto, musicRequestDto, "블라블라");
ItemResponseDto itemResponseDto = createValidItemResponseDto();

given(bannedWordRepository.findBannedWordsInWordList(any())).willReturn(List.of());
given(itemService.create(mockUser, itemRequestDto)).willReturn(itemResponseDto);

var response = mvc.perform(
Expand All @@ -173,6 +188,7 @@ void createItem_InvalidLatitudeRequest_ReturnsBadRequest() throws Exception {
ItemCreateRequestDto itemRequestDto = new ItemCreateRequestDto(itemLocationRequestDto, musicRequestDto, "블라블라");
ItemResponseDto itemResponseDto = createValidItemResponseDto();

given(bannedWordRepository.findBannedWordsInWordList(any())).willReturn(List.of());
given(itemService.create(mockUser, itemRequestDto)).willReturn(itemResponseDto);

var response = mvc.perform(
Expand All @@ -192,6 +208,7 @@ void createItem_InvalidLogitudeRequest_ReturnsBadRequest() throws Exception {
ItemCreateRequestDto itemRequestDto = new ItemCreateRequestDto(itemLocationRequestDto, musicRequestDto, "블라블라");
ItemResponseDto itemResponseDto = createValidItemResponseDto();

given(bannedWordRepository.findBannedWordsInWordList(any())).willReturn(List.of());
given(itemService.create(mockUser, itemRequestDto)).willReturn(itemResponseDto);

var response = mvc.perform(
Expand All @@ -211,6 +228,7 @@ void createItem_InvalidAddressRequest_ReturnsBadRequest() throws Exception {
ItemCreateRequestDto itemRequestDto = new ItemCreateRequestDto(itemLocationRequestDto, musicRequestDto, "블라블라");
ItemResponseDto itemResponseDto = createValidItemResponseDto();

given(bannedWordRepository.findBannedWordsInWordList(any())).willReturn(List.of());
given(itemService.create(mockUser, itemRequestDto)).willReturn(itemResponseDto);

var response = mvc.perform(
Expand All @@ -237,8 +255,29 @@ void createItem_InvalidContentRequest_ReturnsBadRequest() throws Exception {
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(itemRequestDto)));

response.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value("Content is required"));

System.out.println(response.andReturn().getResponse().getContentAsString());
response.andExpect(jsonPath("$.message").value("Content is required")).andExpect(status().isBadRequest())
;
}

@DisplayName("컨텐츠 유효성 검사 실패 - 금칙어 사용된 경우")
@Test
void createItem_BannedWordInclude_ReturnsBadRequest() throws Exception {
MusicRequestDto musicRequestDto = new MusicRequestDto("Love Dive", "IVE", "1st EP IVE", "https://www.youtube.com/watch?v=YGieI3KoeZk", List.of("K-POP", "HipHop"));
ItemLocationRequestDto itemLocationRequestDto = new ItemLocationRequestDto(37.123456, 127.123456, "서울시 성수동 성수 1가");
ItemCreateRequestDto itemRequestDto = new ItemCreateRequestDto(itemLocationRequestDto, musicRequestDto, "나쁜 말");
ItemResponseDto itemResponseDto = createValidItemResponseDto();

given(bannedWordRepository.findBannedWordsInWordList(any())).willReturn(List.of("나쁜"));
given(itemService.create(mockUser, itemRequestDto)).willReturn(itemResponseDto);

var response = mvc.perform(
post("/items")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(itemRequestDto)));

response.andExpect(status().isBadRequest());
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.depromeet.common.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import static jakarta.persistence.GenerationType.IDENTITY;
import static lombok.AccessLevel.PROTECTED;

@Getter
@NoArgsConstructor(access = PROTECTED)
@Entity
@Table(indexes = {
@Index(name = "idx__banned_word_word", columnList = "word")
})
public class BannedWord {
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "banned_word_id")
private Long id;

@Column(length = 20, nullable = false)
private String word;
}

0 comments on commit 49e4134

Please sign in to comment.