diff --git a/capturecat-core/build.gradle b/capturecat-core/build.gradle index 862472c..00034e4 100644 --- a/capturecat-core/build.gradle +++ b/capturecat-core/build.gradle @@ -29,4 +29,5 @@ dependencies { implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" annotationProcessor 'jakarta.persistence:jakarta.persistence-api' + implementation 'org.flywaydb:flyway-database-postgresql' } diff --git a/capturecat-core/src/docs/asciidoc/error-codes.adoc b/capturecat-core/src/docs/asciidoc/error-codes.adoc index 70fd9b5..e1e9d30 100644 --- a/capturecat-core/src/docs/asciidoc/error-codes.adoc +++ b/capturecat-core/src/docs/asciidoc/error-codes.adoc @@ -89,3 +89,7 @@ include::{snippets}/errorCode/deleteTag/error-codes.adoc[] [[검색어-자동완성]] === 검색어 자동완성 include::{snippets}/errorCode/autocomplete/error-codes.adoc[] + +[[유저-태그-생성]] +=== 유저 태그 생성 +include::{snippets}/errorCode/createUserTag/error-codes.adoc[] diff --git a/capturecat-core/src/docs/asciidoc/user.adoc b/capturecat-core/src/docs/asciidoc/user.adoc index e2856a2..8db3538 100644 --- a/capturecat-core/src/docs/asciidoc/user.adoc +++ b/capturecat-core/src/docs/asciidoc/user.adoc @@ -11,7 +11,6 @@ operation::tutorialComplete[snippets='curl-request,http-request,request-headers, <>를 살펴보세요. - [[회원-탈퇴]] === 회원 탈퇴 소셜 서비스 연결 해제 후 회원 관련 데이터를 삭제 처리 합니다. @@ -37,3 +36,15 @@ operation::userInfo[snippets='curl-request,http-request,request-headers,http-res 사용자 정보 조회가 실패했다면 HTTP 상태 코드와 함께 <<에러-객체-형식, 에러 객체>>가 돌아옵니다. <>를 살펴보세요. + +[[유저-태그-생성]] +=== 유저 태그 생성 +사용자의 태그를 생성합니다. + +==== 성공 +operation::createUserTag[snippets='curl-request,http-request,request-headers,query-parameters,http-response,response-fields'] + +==== 실패 +유저 태그 생성이 실패했다면 HTTP 상태 코드와 함께 <<에러-객체-형식, 에러 객체>>가 돌아옵니다. + +<>를 살펴보세요. diff --git a/capturecat-core/src/main/java/com/capturecat/core/api/user/UserTagController.java b/capturecat-core/src/main/java/com/capturecat/core/api/user/UserTagController.java new file mode 100644 index 0000000..243b33a --- /dev/null +++ b/capturecat-core/src/main/java/com/capturecat/core/api/user/UserTagController.java @@ -0,0 +1,29 @@ +package com.capturecat.core.api.user; + +import org.springframework.security.core.annotation.AuthenticationPrincipal; +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; + +import lombok.RequiredArgsConstructor; + +import com.capturecat.core.service.auth.LoginUser; +import com.capturecat.core.service.image.TagResponse; +import com.capturecat.core.service.user.UserTagService; +import com.capturecat.core.support.response.ApiResponse; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/v1/user-tags") +public class UserTagController { + + private final UserTagService userTagService; + + @PostMapping + public ApiResponse create(@AuthenticationPrincipal LoginUser loginUser, @RequestParam String tagName) { + TagResponse tagResponse = userTagService.create(loginUser, tagName); + + return ApiResponse.success(tagResponse); + } +} diff --git a/capturecat-core/src/main/java/com/capturecat/core/domain/tag/TagRegister.java b/capturecat-core/src/main/java/com/capturecat/core/domain/tag/TagRegister.java index 899a133..09cb6a6 100644 --- a/capturecat-core/src/main/java/com/capturecat/core/domain/tag/TagRegister.java +++ b/capturecat-core/src/main/java/com/capturecat/core/domain/tag/TagRegister.java @@ -38,4 +38,15 @@ public List registerTagsFor(List tagNames) { result.addAll(savedNewTags); return result; } + + /** + * 등록되지 않은 태그는 등록하고, 이미 존재하는 태그는 그대로 반환합니다. + * @param tagName 태그 이름 + * @return 조회되거나 새로 생성된 {@link Tag} 엔티티 + */ + @Transactional + public Tag registerTagsFor(String tagName) { + return tagRepository.findByName(tagName) + .orElseGet(() -> tagRepository.save(new Tag(tagName))); + } } diff --git a/capturecat-core/src/main/java/com/capturecat/core/domain/tag/TagRepository.java b/capturecat-core/src/main/java/com/capturecat/core/domain/tag/TagRepository.java index b58f04c..d461d99 100644 --- a/capturecat-core/src/main/java/com/capturecat/core/domain/tag/TagRepository.java +++ b/capturecat-core/src/main/java/com/capturecat/core/domain/tag/TagRepository.java @@ -1,10 +1,13 @@ package com.capturecat.core.domain.tag; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; public interface TagRepository extends JpaRepository, TagCustomRepository { List findByNameIn(List names); + + Optional findByName(String name); } diff --git a/capturecat-core/src/main/java/com/capturecat/core/domain/user/UserTag.java b/capturecat-core/src/main/java/com/capturecat/core/domain/user/UserTag.java new file mode 100644 index 0000000..dea28ca --- /dev/null +++ b/capturecat-core/src/main/java/com/capturecat/core/domain/user/UserTag.java @@ -0,0 +1,48 @@ +package com.capturecat.core.domain.user; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import com.capturecat.core.domain.BaseTimeEntity; +import com.capturecat.core.domain.tag.Tag; + +@Entity +@Table(name = "user_tag", + uniqueConstraints = @UniqueConstraint(name = "uk_user_tag_user_tag", columnNames = {"user_id", "tag_id"}) +) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class UserTag extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "tag_id") + private Tag tag; + + private UserTag(User user, Tag tag) { + this.user = user; + this.tag = tag; + } + + public static UserTag create(User user, Tag tag) { + return new UserTag(user, tag); + } +} diff --git a/capturecat-core/src/main/java/com/capturecat/core/domain/user/UserTagRepository.java b/capturecat-core/src/main/java/com/capturecat/core/domain/user/UserTagRepository.java new file mode 100644 index 0000000..e0ececa --- /dev/null +++ b/capturecat-core/src/main/java/com/capturecat/core/domain/user/UserTagRepository.java @@ -0,0 +1,12 @@ +package com.capturecat.core.domain.user; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.capturecat.core.domain.tag.Tag; + +public interface UserTagRepository extends JpaRepository { + + boolean existsByUserAndTag(User user, Tag tag); + + long countByUser(User user); +} diff --git a/capturecat-core/src/main/java/com/capturecat/core/service/user/UserTagService.java b/capturecat-core/src/main/java/com/capturecat/core/service/user/UserTagService.java new file mode 100644 index 0000000..0ec9fd8 --- /dev/null +++ b/capturecat-core/src/main/java/com/capturecat/core/service/user/UserTagService.java @@ -0,0 +1,67 @@ +package com.capturecat.core.service.user; + +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import com.capturecat.core.domain.tag.Tag; +import com.capturecat.core.domain.tag.TagRegister; +import com.capturecat.core.domain.user.User; +import com.capturecat.core.domain.user.UserRepository; +import com.capturecat.core.domain.user.UserTag; +import com.capturecat.core.domain.user.UserTagRepository; +import com.capturecat.core.service.auth.LoginUser; +import com.capturecat.core.service.image.TagResponse; +import com.capturecat.core.support.error.CoreException; +import com.capturecat.core.support.error.ErrorType; + +@Slf4j +@Service +@RequiredArgsConstructor +public class UserTagService { + + private static final int MAX_USER_TAG_COUNT = 30; + + private final UserRepository userRepository; + private final UserTagRepository userTagRepository; + private final TagRegister tagRegister; + + @Transactional + public TagResponse create(LoginUser loginUser, String tagName) { + try { + User user = userRepository.findByUsername(loginUser.getUsername()) + .orElseThrow(() -> new CoreException(ErrorType.USER_NOT_FOUND)); + Tag tag = tagRegister.registerTagsFor(tagName); + + validate(user, tag); + + userTagRepository.save(UserTag.create(user, tag)); + + return TagResponse.from(tag); + } catch (DataIntegrityViolationException ex) { + throw new CoreException(ErrorType.USER_TAG_ALREADY_EXISTS); + } + } + + private void validate(User user, Tag tag) { + validateDuplicateUserTag(user, tag); + validateUserTagCountLimit(user); + } + + private void validateDuplicateUserTag(User user, Tag tag) { + if (userTagRepository.existsByUserAndTag(user, tag)) { + throw new CoreException(ErrorType.USER_TAG_ALREADY_EXISTS); + } + } + + private void validateUserTagCountLimit(User user) { + long userTagCount = userTagRepository.countByUser(user); + + if (userTagCount >= MAX_USER_TAG_COUNT) { + throw new CoreException(ErrorType.TOO_MANY_USER_TAGS); + } + } +} diff --git a/capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorCode.java b/capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorCode.java index 91c10af..8a8aa63 100644 --- a/capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorCode.java +++ b/capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorCode.java @@ -38,7 +38,9 @@ public enum ErrorCode { SOCIAL_API_ERROR("소셜 서비스 API 호출 결과 실패를 응답받았습니다."), MISSING_PARAMETER("필수 파라미터 %s(이)가 누락되었습니다."), INTERNAL_SERVER_ERROR("서버에서 오류가 발생했습니다."), - INVALID_LOGOUT_AUTH_TOKEN("ACCESS 토큰 또는 REFRESH 토큰이 유효하지 않습니다."); + INVALID_LOGOUT_AUTH_TOKEN("ACCESS 토큰 또는 REFRESH 토큰이 유효하지 않습니다."), + ALREADY_EXISTS_USER_TAG("이미 등록된 유저 태그입니다."), + EXCEED_MAX_USER_TAG_COUNT("태그는 한 계정당 최대 30개까지 추가할 수 있어요."); private final String message; diff --git a/capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorType.java b/capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorType.java index 522ce37..da6b01e 100644 --- a/capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorType.java +++ b/capturecat-core/src/main/java/com/capturecat/core/support/error/ErrorType.java @@ -38,11 +38,14 @@ public enum ErrorType { INVALID_LOGOUT_AUTH_TOKEN(HttpStatus.UNAUTHORIZED, ErrorCode.INVALID_LOGOUT_AUTH_TOKEN, LogLevel.WARN), BOOKMARK_NOT_FOUND(HttpStatus.NOT_FOUND, ErrorCode.NOT_FOUND_BOOKMARK, LogLevel.WARN), GENERATE_CLIENT_SECRET_FAIL(HttpStatus.INTERNAL_SERVER_ERROR, ErrorCode.GENERATE_CLIENT_SECRET_FAIL, - LogLevel.ERROR), UNLINK_SOCIAL_FAIL(HttpStatus.BAD_REQUEST, ErrorCode.UNLINK_SOCIAL_FAIL, LogLevel.WARN), + LogLevel.ERROR), + UNLINK_SOCIAL_FAIL(HttpStatus.BAD_REQUEST, ErrorCode.UNLINK_SOCIAL_FAIL, LogLevel.WARN), FETCH_SOCIAL_TOKEN_FAIL(HttpStatus.BAD_REQUEST, ErrorCode.FETCH_SOCIAL_TOKEN_FAIL, LogLevel.WARN), SOCIAL_API_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, ErrorCode.SOCIAL_API_ERROR, LogLevel.WARN), MISSING_PARAMETER(HttpStatus.BAD_REQUEST, ErrorCode.MISSING_PARAMETER, LogLevel.WARN), - INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, ErrorCode.INTERNAL_SERVER_ERROR, LogLevel.ERROR); + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, ErrorCode.INTERNAL_SERVER_ERROR, LogLevel.ERROR), + USER_TAG_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, ErrorCode.ALREADY_EXISTS_USER_TAG, LogLevel.WARN), + TOO_MANY_USER_TAGS(HttpStatus.BAD_REQUEST, ErrorCode.EXCEED_MAX_USER_TAG_COUNT, LogLevel.WARN); private final HttpStatus status; diff --git a/capturecat-core/src/main/resources/db/migration/V1__init.sql b/capturecat-core/src/main/resources/db/migration/V1__init.sql new file mode 100644 index 0000000..b897221 --- /dev/null +++ b/capturecat-core/src/main/resources/db/migration/V1__init.sql @@ -0,0 +1,87 @@ +create table if not exists users +( + id bigint not null primary key, + nickname varchar(50) not null, + email varchar(50) not null, + username varchar(50) not null unique, + password varchar(70), + role varchar(255) not null constraint users_role_check check (role in ('ADMIN', 'PREMIUM_USER', 'USER')), + tutorial_completed boolean not null, + provider varchar(255), + social_id varchar(255), + created_date timestamp(6) not null, + last_modified_date timestamp(6) not null +); + +create table if not exists user_social_account +( + id bigint not null primary key, + created_date timestamp(6) not null, + last_modified_date timestamp(6) not null, + provider varchar(30) not null, + social_id varchar(100) not null, + unlink_key varchar(512), + user_id bigint not null constraint fk998rgv7jn090iyc77f8e1xsnq references users, + constraint uksj2lqxj8h0xuqf9v1dvtlkegt unique (provider, social_id) +); + +create table if not exists refresh_token +( + id bigint not null primary key, + refresh_token_expiration bigint not null, + expiration varchar(255), + refresh_token varchar(255), + username varchar(255) +); + +create table if not exists withdraw_log +( + id bigint generated by default as identity primary key, + created_date timestamp(6) not null, + last_modified_date timestamp(6) not null, + image_cleanup_status varchar(255) not null constraint withdraw_log_image_cleanup_status_check check (image_cleanup_status in ('PENDING', 'DONE', 'FAILED')), + reason text, + user_id bigint not null +); + +create index if not exists idx_withdraw_log_user_id on withdraw_log (user_id); + +create index if not exists idx_withdraw_log_created_date on withdraw_log (created_date); + +create index if not exists idx_withdraw_log_cleanup_status on withdraw_log (image_cleanup_status, created_date); + +create table if not exists images +( + id bigint generated by default as identity primary key, + size bigint, + file_name varchar(255), + file_url varchar(255), + capture_date date, + created_date timestamp(6) not null, + last_modified_date timestamp(6) not null, + user_id bigint constraint fk13ljqfrfwbyvnsdhihwta8cpr references users +); + +create table if not exists tag +( + id bigint generated by default as identity primary key, + name varchar(255), + created_date timestamp(6) not null, + last_modified_date timestamp(6) not null +); + +create table if not exists image_tag +( + id bigint generated by default as identity primary key, + image_id bigint constraint fk6q9wuvp5j846qtqod6xu3gma1 references images, + tag_id bigint constraint fk28yowgjl7oksr7dc0wj7f5il references tag, + created_date timestamp(6) not null, + last_modified_date timestamp(6) not null +); + +create table if not exists bookmark +( + id bigint generated by default as identity primary key, + image_id bigint constraint fkpowbsxsu0qwcon1yoxbsqkw4w references images, + user_id bigint constraint fko4vbqvq5trl11d85bqu5kl870 references users +); diff --git a/capturecat-core/src/main/resources/db/migration/V2__create_user_tag.sql b/capturecat-core/src/main/resources/db/migration/V2__create_user_tag.sql new file mode 100644 index 0000000..de8b7de --- /dev/null +++ b/capturecat-core/src/main/resources/db/migration/V2__create_user_tag.sql @@ -0,0 +1,9 @@ +create table if not exists user_tag +( + id bigint generated by default as identity primary key, + user_id bigint constraint fk_user_tag_user references users, + tag_id bigint constraint fk_user_tag_tag references tag, + created_date timestamp(6) not null, + last_modified_date timestamp(6) not null, + constraint uk_user_tag_user_tag unique (user_id, tag_id) +); diff --git a/capturecat-core/src/test/java/com/capturecat/core/api/user/UserErrorCodeControllerTest.java b/capturecat-core/src/test/java/com/capturecat/core/api/user/UserErrorCodeControllerTest.java new file mode 100644 index 0000000..0e4aae8 --- /dev/null +++ b/capturecat-core/src/test/java/com/capturecat/core/api/user/UserErrorCodeControllerTest.java @@ -0,0 +1,22 @@ +package com.capturecat.core.api.user; + +import static com.capturecat.core.support.error.ErrorType.TOO_MANY_USER_TAGS; +import static com.capturecat.core.support.error.ErrorType.USER_NOT_FOUND; +import static com.capturecat.core.support.error.ErrorType.USER_TAG_ALREADY_EXISTS; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.capturecat.core.api.error.ErrorCodeDocumentTest; +import com.capturecat.test.snippet.ErrorCodeDescriptor; + +class UserErrorCodeControllerTest extends ErrorCodeDocumentTest { + + @Test + void 유저_태그_생성_에러_코드_문서화() { + List errorCodeDescriptors = generateErrorCodeDescriptors(USER_TAG_ALREADY_EXISTS, + TOO_MANY_USER_TAGS, USER_NOT_FOUND); + generateErrorDocs("errorCode/createUserTag", errorCodeDescriptors); + } +} diff --git a/capturecat-core/src/test/java/com/capturecat/core/api/user/UserTagControllerTest.java b/capturecat-core/src/test/java/com/capturecat/core/api/user/UserTagControllerTest.java new file mode 100644 index 0000000..719bbb8 --- /dev/null +++ b/capturecat-core/src/test/java/com/capturecat/core/api/user/UserTagControllerTest.java @@ -0,0 +1,65 @@ +package com.capturecat.core.api.user; + +import static com.capturecat.test.api.RestDocsUtil.requestPreprocessor; +import static com.capturecat.test.api.RestDocsUtil.responsePreprocessor; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.BDDMockito; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.restdocs.payload.JsonFieldType; + +import io.restassured.http.ContentType; + +import com.capturecat.core.config.jwt.JwtUtil; +import com.capturecat.core.service.image.TagResponse; +import com.capturecat.core.service.user.UserTagService; +import com.capturecat.test.api.RestDocsTest; + +class UserTagControllerTest extends RestDocsTest { + + private static final String ACCESS_TOKEN = "valid-access-token"; + + private UserTagController userTagController; + private UserTagService userTagService; + + @BeforeEach + void setUp() { + userTagService = mock(UserTagService.class); + userTagController = new UserTagController(userTagService); + mockMvc = mockController(userTagController); + } + + @Test + void 유저_태그_생성() { + // given + BDDMockito.given(userTagService.create(any(), anyString())).willReturn(new TagResponse(1L, "java")); + + // when & then + given() + .header(HttpHeaders.AUTHORIZATION, JwtUtil.BEARER_PREFIX + ACCESS_TOKEN) + .contentType(ContentType.JSON) + .queryParam("tagName", "java") + .when().post("/v1/user-tags") + .then().status(HttpStatus.OK) + .apply(document("createUserTag", requestPreprocessor(), responsePreprocessor(), + requestHeaders(headerWithName(HttpHeaders.AUTHORIZATION).description("유효한 Access 토큰")), + queryParameters(parameterWithName("tagName").description("태그 이름")), + responseFields( + fieldWithPath("result").type(JsonFieldType.STRING).description("요청 결과"), + fieldWithPath("data").type(JsonFieldType.OBJECT).description("사용자가 등록한 태그 정보"), + fieldWithPath("data.id").type(JsonFieldType.NUMBER).description("태그 ID"), + fieldWithPath("data.name").type(JsonFieldType.STRING).description("태그 이름")))); + } +} diff --git a/capturecat-core/src/test/java/com/capturecat/core/domain/tag/TagRegisterTest.java b/capturecat-core/src/test/java/com/capturecat/core/domain/tag/TagRegisterTest.java index a468290..096457c 100644 --- a/capturecat-core/src/test/java/com/capturecat/core/domain/tag/TagRegisterTest.java +++ b/capturecat-core/src/test/java/com/capturecat/core/domain/tag/TagRegisterTest.java @@ -1,13 +1,16 @@ package com.capturecat.core.domain.tag; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import java.util.Collections; import java.util.List; +import java.util.Optional; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -86,4 +89,41 @@ class TagRegisterTest { assertThat(resultTags).extracting(Tag::getName) .containsExactlyInAnyOrderElementsOf(tagNames); } + + @Test + void 태그가_저장되지_않은_경우_태그를_저장한다() { + // given + String tagName = "단일태그"; + + given(tagRepository.findByName(tagName)).willReturn(Optional.empty()); + given(tagRepository.save(any())).willReturn(TagFixture.createTag(1L, tagName)); + + // when + Tag resultTag = tagRegister.registerTagsFor(tagName); + + // then + assertThat(resultTag.getId()).isNotNull(); + assertThat(resultTag.getName()).isEqualTo(tagName); + + verify(tagRepository, times(1)).findByName(tagName); + verify(tagRepository, times(1)).save(any()); + } + + @Test + void 이미_저장된_태그인_경우_태그_엔티티를_반환한다() { + // given + String tagName = "기존태그"; + + given(tagRepository.findByName(tagName)).willReturn(Optional.of(TagFixture.createTag(1L, tagName))); + + // when + Tag resultTag = tagRegister.registerTagsFor(tagName); + + // then + assertThat(resultTag.getId()).isNotNull(); + assertThat(resultTag.getName()).isEqualTo(tagName); + + verify(tagRepository, times(1)).findByName(tagName); + verify(tagRepository, never()).save(any()); + } } diff --git a/capturecat-core/src/test/java/com/capturecat/core/domain/user/UserTagFixture.java b/capturecat-core/src/test/java/com/capturecat/core/domain/user/UserTagFixture.java new file mode 100644 index 0000000..82f3dc2 --- /dev/null +++ b/capturecat-core/src/test/java/com/capturecat/core/domain/user/UserTagFixture.java @@ -0,0 +1,14 @@ +package com.capturecat.core.domain.user; + +import org.springframework.test.util.ReflectionTestUtils; + +import com.capturecat.core.domain.tag.Tag; + +public class UserTagFixture { + + public static UserTag createUserTag(Long id, User user, Tag tag) { + UserTag userTag = UserTag.create(user, tag); + ReflectionTestUtils.setField(userTag, "id", id); + return userTag; + } +} diff --git a/capturecat-core/src/test/java/com/capturecat/core/service/user/UserTagServiceTest.java b/capturecat-core/src/test/java/com/capturecat/core/service/user/UserTagServiceTest.java new file mode 100644 index 0000000..8ca364e --- /dev/null +++ b/capturecat-core/src/test/java/com/capturecat/core/service/user/UserTagServiceTest.java @@ -0,0 +1,117 @@ +package com.capturecat.core.service.user; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.util.Optional; + +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 com.capturecat.core.DummyObject; +import com.capturecat.core.domain.tag.TagFixture; +import com.capturecat.core.domain.tag.TagRegister; +import com.capturecat.core.domain.user.UserRepository; +import com.capturecat.core.domain.user.UserTagFixture; +import com.capturecat.core.domain.user.UserTagRepository; +import com.capturecat.core.service.auth.LoginUser; +import com.capturecat.core.support.error.CoreException; +import com.capturecat.core.support.error.ErrorType; + +@ExtendWith(MockitoExtension.class) +class UserTagServiceTest { + + @Mock + UserRepository userRepository; + + @Mock + UserTagRepository userTagRepository; + + @Mock + TagRegister tagRegister; + + @InjectMocks + UserTagService userTagService; + + @Test + void 유저_태그를_생성한다() { + // given + var user = DummyObject.newUser("test"); + var tag = TagFixture.createTag(1L, "java"); + + given(userRepository.findByUsername(anyString())).willReturn(Optional.of(user)); + given(tagRegister.registerTagsFor(anyString())).willReturn(tag); + given(userTagRepository.existsByUserAndTag(eq(user), eq(tag))).willReturn(false); + given(userTagRepository.countByUser(eq(user))).willReturn(0L); + given(userTagRepository.save(any())).willReturn(UserTagFixture.createUserTag(1L, user, tag)); + + // when + var response = userTagService.create(new LoginUser(user), "java"); + + // then + assertThat(response.id()).isNotNull(); + assertThat(response.name()).isEqualTo(tag.getName()); + + verify(userTagRepository, times(1)).save(any()); + } + + @Test + void 유저_태그_생성_시_회원이_없으면_실패한다() { + // given + given(userRepository.findByUsername(anyString())).willReturn(Optional.empty()); + + // when & then + assertThatThrownBy(() -> userTagService.create(new LoginUser(DummyObject.newUser("test")), "java")) + .isInstanceOf(CoreException.class) + .hasMessage(ErrorType.USER_NOT_FOUND.getCode().getMessage()); + + verify(userTagRepository, never()).save(any()); + } + + @Test + void 유저_태그_생성_시_이미_등록된_경우_실패한다() { + // given + var user = DummyObject.newUser("test"); + var tag = TagFixture.createTag(1L, "java"); + + given(userRepository.findByUsername(anyString())).willReturn(Optional.of(user)); + given(tagRegister.registerTagsFor(anyString())).willReturn(tag); + given(userTagRepository.existsByUserAndTag(eq(user), eq(tag))).willReturn(true); + + // when & then + assertThatThrownBy(() -> userTagService.create(new LoginUser(DummyObject.newUser("test")), "java")) + .isInstanceOf(CoreException.class) + .hasMessage(ErrorType.USER_TAG_ALREADY_EXISTS.getCode().getMessage()); + + verify(userTagRepository, never()).save(any()); + } + + @Test + void 유저_태그_생성_시_최대_등록_개수를_초과하면_실패한다() { + // given + var user = DummyObject.newUser("test"); + var tag = TagFixture.createTag(1L, "java"); + + given(userRepository.findByUsername(anyString())).willReturn(Optional.of(user)); + given(tagRegister.registerTagsFor(anyString())).willReturn(tag); + given(userTagRepository.existsByUserAndTag(eq(user), eq(tag))).willReturn(false); + given(userTagRepository.countByUser(eq(user))).willReturn(30L); + + // when & then + assertThatThrownBy(() -> userTagService.create(new LoginUser(DummyObject.newUser("test")), "java")) + .isInstanceOf(CoreException.class) + .hasMessage(ErrorType.TOO_MANY_USER_TAGS.getCode().getMessage()); + + verify(userTagRepository, never()).save(any()); + } +} diff --git a/capturecat-core/src/test/resources/application.yml b/capturecat-core/src/test/resources/application.yml index 8f145a5..993421b 100644 --- a/capturecat-core/src/test/resources/application.yml +++ b/capturecat-core/src/test/resources/application.yml @@ -35,6 +35,9 @@ spring: host: localhost port: 6379 + flyway: + enabled: false + image: local: base-path: ${user.home}/Desktop/capturecat/upload