diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index 5f7383e..1cf7886 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -93,6 +93,16 @@ operation::link-controller-test/create-hub-link[snippets='http-response,response operation::link-controller-test/find-links[snippets='http-response,response-fields'] +=== 허브 링크 묶음 목록 조회 + +==== request + +operation::link-bundle-controller-test/find-hub-link-bundles[snippets='http-request,request-headers,path-parameters'] + +==== response + +operation::link-bundle-controller-test/find-hub-link-bundles[snippets='http-response,response-fields'] + == 허브 === 허브 생성 diff --git a/src/main/java/com/seong/shoutlink/domain/auth/NullableUser.java b/src/main/java/com/seong/shoutlink/domain/auth/NullableUser.java new file mode 100644 index 0000000..85bb3f5 --- /dev/null +++ b/src/main/java/com/seong/shoutlink/domain/auth/NullableUser.java @@ -0,0 +1,12 @@ +package com.seong.shoutlink.domain.auth; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface NullableUser { + +} diff --git a/src/main/java/com/seong/shoutlink/domain/hub/repository/HubRepositoryImpl.java b/src/main/java/com/seong/shoutlink/domain/hub/repository/HubRepositoryImpl.java index cbd142f..cb68b26 100644 --- a/src/main/java/com/seong/shoutlink/domain/hub/repository/HubRepositoryImpl.java +++ b/src/main/java/com/seong/shoutlink/domain/hub/repository/HubRepositoryImpl.java @@ -3,8 +3,9 @@ import com.seong.shoutlink.domain.hub.Hub; import com.seong.shoutlink.domain.hub.service.HubRepository; import com.seong.shoutlink.domain.hub.service.result.HubPaginationResult; -import com.seong.shoutlink.domain.hubMember.repository.HubMemberEntity; -import com.seong.shoutlink.domain.hubMember.repository.HubMemberJpaRepository; +import com.seong.shoutlink.domain.hubmember.repository.HubMemberEntity; +import com.seong.shoutlink.domain.hubmember.repository.HubMemberJpaRepository; +import com.seong.shoutlink.domain.member.Member; import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; diff --git a/src/main/java/com/seong/shoutlink/domain/hubMember/HubMemberRole.java b/src/main/java/com/seong/shoutlink/domain/hubMember/HubMemberRole.java index bcc990a..3ac2677 100644 --- a/src/main/java/com/seong/shoutlink/domain/hubMember/HubMemberRole.java +++ b/src/main/java/com/seong/shoutlink/domain/hubMember/HubMemberRole.java @@ -1,4 +1,4 @@ -package com.seong.shoutlink.domain.hubMember; +package com.seong.shoutlink.domain.hubmember; public enum HubMemberRole { MASTER, PARTICIPANTS diff --git a/src/main/java/com/seong/shoutlink/domain/hubMember/repository/HubMemberEntity.java b/src/main/java/com/seong/shoutlink/domain/hubMember/repository/HubMemberEntity.java index e5b8071..9f50f29 100644 --- a/src/main/java/com/seong/shoutlink/domain/hubMember/repository/HubMemberEntity.java +++ b/src/main/java/com/seong/shoutlink/domain/hubMember/repository/HubMemberEntity.java @@ -1,8 +1,8 @@ -package com.seong.shoutlink.domain.hubMember.repository; +package com.seong.shoutlink.domain.hubmember.repository; import com.seong.shoutlink.domain.hub.Hub; import com.seong.shoutlink.domain.hub.repository.HubEntity; -import com.seong.shoutlink.domain.hubMember.HubMemberRole; +import com.seong.shoutlink.domain.hubmember.HubMemberRole; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; diff --git a/src/main/java/com/seong/shoutlink/domain/hubMember/repository/HubMemberJpaRepository.java b/src/main/java/com/seong/shoutlink/domain/hubMember/repository/HubMemberJpaRepository.java index 5594679..61ed2bf 100644 --- a/src/main/java/com/seong/shoutlink/domain/hubMember/repository/HubMemberJpaRepository.java +++ b/src/main/java/com/seong/shoutlink/domain/hubMember/repository/HubMemberJpaRepository.java @@ -1,4 +1,4 @@ -package com.seong.shoutlink.domain.hubMember.repository; +package com.seong.shoutlink.domain.hubmember.repository; import java.util.Optional; import org.springframework.data.domain.Page; @@ -12,11 +12,11 @@ public interface HubMemberJpaRepository extends JpaRepository findHubMasterByHubIdWithHub(@Param("hubId") Long hubId); @Query("select hm from HubMemberEntity hm " + "join fetch hm.hubEntity h " - + "where hm.hubMemberRole = com.seong.shoutlink.domain.hubMember.HubMemberRole.MASTER") + + "where hm.hubMemberRole = com.seong.shoutlink.domain.hubmember.HubMemberRole.MASTER") Page findHubs(PageRequest pageRequest); } diff --git a/src/main/java/com/seong/shoutlink/domain/hubMember/repository/HubMemberRepositoryImpl.java b/src/main/java/com/seong/shoutlink/domain/hubMember/repository/HubMemberRepositoryImpl.java new file mode 100644 index 0000000..d1fa21d --- /dev/null +++ b/src/main/java/com/seong/shoutlink/domain/hubMember/repository/HubMemberRepositoryImpl.java @@ -0,0 +1,15 @@ +package com.seong.shoutlink.domain.hubmember.repository; + +import com.seong.shoutlink.domain.hub.Hub; +import com.seong.shoutlink.domain.hubmember.service.HubMemberRepository; +import com.seong.shoutlink.domain.member.Member; +import org.springframework.stereotype.Repository; + +@Repository +public class HubMemberRepositoryImpl implements HubMemberRepository { + + @Override + public boolean isHubMember(Hub hub, Member member) { + return false; + } +} diff --git a/src/main/java/com/seong/shoutlink/domain/hubMember/service/HubMemberRepository.java b/src/main/java/com/seong/shoutlink/domain/hubMember/service/HubMemberRepository.java new file mode 100644 index 0000000..3c81756 --- /dev/null +++ b/src/main/java/com/seong/shoutlink/domain/hubMember/service/HubMemberRepository.java @@ -0,0 +1,9 @@ +package com.seong.shoutlink.domain.hubmember.service; + +import com.seong.shoutlink.domain.hub.Hub; +import com.seong.shoutlink.domain.member.Member; + +public interface HubMemberRepository { + + boolean isHubMember(Hub hub, Member member); +} diff --git a/src/main/java/com/seong/shoutlink/domain/linkbundle/controller/LinkBundleController.java b/src/main/java/com/seong/shoutlink/domain/linkbundle/controller/LinkBundleController.java index cea5c07..6863a70 100644 --- a/src/main/java/com/seong/shoutlink/domain/linkbundle/controller/LinkBundleController.java +++ b/src/main/java/com/seong/shoutlink/domain/linkbundle/controller/LinkBundleController.java @@ -1,9 +1,11 @@ package com.seong.shoutlink.domain.linkbundle.controller; import com.seong.shoutlink.domain.auth.LoginUser; +import com.seong.shoutlink.domain.auth.NullableUser; import com.seong.shoutlink.domain.linkbundle.controller.request.CreateLinkBundleRequest; import com.seong.shoutlink.domain.linkbundle.service.LinkBundleService; import com.seong.shoutlink.domain.linkbundle.service.request.CreateHubLinkBundleCommand; +import com.seong.shoutlink.domain.linkbundle.service.request.FindHubLinkBundlesCommand; import com.seong.shoutlink.domain.linkbundle.service.request.FindLinkBundlesCommand; import com.seong.shoutlink.domain.linkbundle.service.response.CreateLinkBundleCommand; import com.seong.shoutlink.domain.linkbundle.service.response.CreateLinkBundleResponse; @@ -59,4 +61,13 @@ public ResponseEntity createHubLinkBundle( request.isDefault())); return ResponseEntity.status(HttpStatus.CREATED).body(response); } + + @GetMapping("/hubs/{hubId}/link-bundles") + public ResponseEntity findHubLinkBundles( + @NullableUser Long nullableMemberId, + @PathVariable("hubId") Long hubId) { + FindLinkBundlesResponse response = linkBundleService.findHubLinkBundles( + new FindHubLinkBundlesCommand(hubId, nullableMemberId)); + return ResponseEntity.ok(response); + } } diff --git a/src/main/java/com/seong/shoutlink/domain/linkbundle/repository/LinkBundleJpaRepository.java b/src/main/java/com/seong/shoutlink/domain/linkbundle/repository/LinkBundleJpaRepository.java index 0f7830a..270d152 100644 --- a/src/main/java/com/seong/shoutlink/domain/linkbundle/repository/LinkBundleJpaRepository.java +++ b/src/main/java/com/seong/shoutlink/domain/linkbundle/repository/LinkBundleJpaRepository.java @@ -24,4 +24,8 @@ public interface LinkBundleJpaRepository extends JpaRepository findHubLinkBundle( @Param("linkBundleId") Long linkBundleId, @Param("hubId") Long hubId); + + @Query("select lb from HubLinkBundleEntity lb " + + "where lb.hubId = :hubId") + List findAllByHubId(@Param("hubId") Long hubId); } diff --git a/src/main/java/com/seong/shoutlink/domain/linkbundle/repository/LinkBundleRepositoryImpl.java b/src/main/java/com/seong/shoutlink/domain/linkbundle/repository/LinkBundleRepositoryImpl.java index 7894ee9..f607e67 100644 --- a/src/main/java/com/seong/shoutlink/domain/linkbundle/repository/LinkBundleRepositoryImpl.java +++ b/src/main/java/com/seong/shoutlink/domain/linkbundle/repository/LinkBundleRepositoryImpl.java @@ -55,4 +55,12 @@ public Long save(HubLinkBundle hubLinkBundle) { public Optional findHubLinkBundle(Long linkBundleId, Hub hub) { return linkBundleJpaRepository.findHubLinkBundle(linkBundleId, hub.getHubId()); } + + @Override + public List findHubLinkBundles(Hub hub) { + return linkBundleJpaRepository.findAllByHubId(hub.getHubId()) + .stream() + .map(LinkBundleEntity::toDomain) + .toList(); + } } diff --git a/src/main/java/com/seong/shoutlink/domain/linkbundle/service/LinkBundleRepository.java b/src/main/java/com/seong/shoutlink/domain/linkbundle/service/LinkBundleRepository.java index 3410180..a69cdcc 100644 --- a/src/main/java/com/seong/shoutlink/domain/linkbundle/service/LinkBundleRepository.java +++ b/src/main/java/com/seong/shoutlink/domain/linkbundle/service/LinkBundleRepository.java @@ -21,4 +21,6 @@ public interface LinkBundleRepository { Long save(HubLinkBundle hubLinkBundle); Optional findHubLinkBundle(Long linkBundleId, Hub hub); + + List findHubLinkBundles(Hub hubId); } diff --git a/src/main/java/com/seong/shoutlink/domain/linkbundle/service/LinkBundleService.java b/src/main/java/com/seong/shoutlink/domain/linkbundle/service/LinkBundleService.java index f7d94da..0a4acdb 100644 --- a/src/main/java/com/seong/shoutlink/domain/linkbundle/service/LinkBundleService.java +++ b/src/main/java/com/seong/shoutlink/domain/linkbundle/service/LinkBundleService.java @@ -4,17 +4,21 @@ import com.seong.shoutlink.domain.exception.ShoutLinkException; import com.seong.shoutlink.domain.hub.Hub; import com.seong.shoutlink.domain.hub.service.HubRepository; +import com.seong.shoutlink.domain.hubmember.service.HubMemberRepository; import com.seong.shoutlink.domain.linkbundle.HubLinkBundle; import com.seong.shoutlink.domain.linkbundle.LinkBundle; import com.seong.shoutlink.domain.linkbundle.MemberLinkBundle; import com.seong.shoutlink.domain.linkbundle.service.request.CreateHubLinkBundleCommand; +import com.seong.shoutlink.domain.linkbundle.service.request.FindHubLinkBundlesCommand; import com.seong.shoutlink.domain.linkbundle.service.request.FindLinkBundlesCommand; import com.seong.shoutlink.domain.linkbundle.service.response.CreateLinkBundleCommand; import com.seong.shoutlink.domain.linkbundle.service.response.CreateLinkBundleResponse; import com.seong.shoutlink.domain.linkbundle.service.response.FindLinkBundlesResponse; import com.seong.shoutlink.domain.member.Member; import com.seong.shoutlink.domain.member.service.MemberRepository; +import jakarta.annotation.Nullable; import java.util.List; +import java.util.Objects; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -25,6 +29,7 @@ public class LinkBundleService { private final MemberRepository memberRepository; private final HubRepository hubRepository; + private final HubMemberRepository hubMemberRepository; private final LinkBundleRepository linkBundleRepository; @Transactional @@ -63,6 +68,32 @@ public CreateLinkBundleResponse createHubLinkBundle(CreateHubLinkBundleCommand c return new CreateLinkBundleResponse(linkBundleRepository.save(hubLinkBundle)); } + @Transactional(readOnly = true) + public FindLinkBundlesResponse findHubLinkBundles(FindHubLinkBundlesCommand command) { + Hub hub = getHub(command.hubId()); + if(hub.isPrivate()) { + checkAuthenticated(command.nullableMemberId()); + Long memberId = command.nullableMemberId(); + Member member = getMember(memberId); + checkHubMemberAuthority(hub, member); + } + List hubLinkBundles = linkBundleRepository.findHubLinkBundles(hub); + return FindLinkBundlesResponse.from(hubLinkBundles); + } + + private void checkAuthenticated(@Nullable Long memberId) { + if(Objects.isNull(memberId)) { + throw new ShoutLinkException("인증되지 않은 회원입니다.", ErrorCode.UNAUTHENTICATED); + } + } + + private void checkHubMemberAuthority(Hub hub, Member member) { + if(hubMemberRepository.isHubMember(hub, member)) { + return; + } + throw new ShoutLinkException("권한이 없습니다.", ErrorCode.UNAUTHORIZED); + } + private Hub getHub(Long hubId) { return hubRepository.findById(hubId) .orElseThrow(() -> new ShoutLinkException("존재하지 않는 허브입니다.", ErrorCode.NOT_FOUND)); diff --git a/src/main/java/com/seong/shoutlink/domain/linkbundle/service/request/FindHubLinkBundlesCommand.java b/src/main/java/com/seong/shoutlink/domain/linkbundle/service/request/FindHubLinkBundlesCommand.java new file mode 100644 index 0000000..85bc57b --- /dev/null +++ b/src/main/java/com/seong/shoutlink/domain/linkbundle/service/request/FindHubLinkBundlesCommand.java @@ -0,0 +1,7 @@ +package com.seong.shoutlink.domain.linkbundle.service.request; + +import jakarta.annotation.Nullable; + +public record FindHubLinkBundlesCommand(Long hubId, @Nullable Long nullableMemberId) { + +} diff --git a/src/main/java/com/seong/shoutlink/global/auth/resolver/NullableUserArgumentResolver.java b/src/main/java/com/seong/shoutlink/global/auth/resolver/NullableUserArgumentResolver.java new file mode 100644 index 0000000..7a4e1d6 --- /dev/null +++ b/src/main/java/com/seong/shoutlink/global/auth/resolver/NullableUserArgumentResolver.java @@ -0,0 +1,35 @@ +package com.seong.shoutlink.global.auth.resolver; + +import com.seong.shoutlink.domain.auth.NullableUser; +import com.seong.shoutlink.global.auth.authentication.Authentication; +import com.seong.shoutlink.global.auth.authentication.AuthenticationContext; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@RequiredArgsConstructor +public class NullableUserArgumentResolver implements HandlerMethodArgumentResolver { + + private final AuthenticationContext authenticationContext; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + boolean hasParameterAnnotation = parameter.hasParameterAnnotation(NullableUser.class); + boolean hasLongParameterType = parameter.getParameterType().isAssignableFrom(Long.class); + return hasParameterAnnotation && hasLongParameterType; + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { + Authentication authentication = authenticationContext.getAuthentication(); + if(Objects.isNull(authentication)) { + return null; + } + return authentication.getPrincipal(); + } +} diff --git a/src/main/java/com/seong/shoutlink/global/config/WebConfig.java b/src/main/java/com/seong/shoutlink/global/config/WebConfig.java index 493ecd7..6fcf04b 100644 --- a/src/main/java/com/seong/shoutlink/global/config/WebConfig.java +++ b/src/main/java/com/seong/shoutlink/global/config/WebConfig.java @@ -4,6 +4,7 @@ import com.seong.shoutlink.global.auth.authentication.AuthenticationContext; import com.seong.shoutlink.global.auth.authentication.JwtAuthenticationInterceptor; import com.seong.shoutlink.global.auth.authentication.JwtAuthenticationProvider; +import com.seong.shoutlink.global.auth.resolver.NullableUserArgumentResolver; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; @@ -30,6 +31,7 @@ public void addInterceptors(InterceptorRegistry registry) { @Override public void addArgumentResolvers(List resolvers) { resolvers.add(new LoginUserArgumentResolver(authenticationContext)); + resolvers.add(new NullableUserArgumentResolver(authenticationContext)); } @Override diff --git a/src/main/resources/static/docs/index.html b/src/main/resources/static/docs/index.html index 4c73c14..c6f4ffd 100644 --- a/src/main/resources/static/docs/index.html +++ b/src/main/resources/static/docs/index.html @@ -468,6 +468,7 @@

API 문서

  • 링크 생성
  • 링크 목록 조회
  • 허브 링크 생성
  • +
  • 허브 링크 묶음 목록 조회
  • 허브 @@ -706,7 +707,7 @@
    POST /api/link-bundles HTTP/1.1
     Content-Type: application/json;charset=UTF-8
    -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzA4MDkwNzc0LCJzdWIiOiIxIiwiZXhwIjoxNzA4MDk0Mzc0LCJyb2xlIjoiUk9MRV9VU0VSIn0.VhlHhySUpcDTp7n5FzwbQFa5e8qqX2ANyR1ZtD3gIbs
    +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzA4MjY1MDI1LCJzdWIiOiIxIiwiZXhwIjoxNzA4MjY4NjI1LCJyb2xlIjoiUk9MRV9VU0VSIn0.jZ4zWxq2NUNkY4APeYGzc2PX_JJqwmYESPChjvJaz4A
     Content-Length: 57
     Host: localhost:8080
     
    @@ -822,7 +823,7 @@ 
    GET /api/link-bundles HTTP/1.1
    -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzA4MDkwNzc0LCJzdWIiOiIxIiwiZXhwIjoxNzA4MDk0Mzc0LCJyb2xlIjoiUk9MRV9VU0VSIn0.VhlHhySUpcDTp7n5FzwbQFa5e8qqX2ANyR1ZtD3gIbs
    +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzA4MjY1MDI1LCJzdWIiOiIxIiwiZXhwIjoxNzA4MjY4NjI1LCJyb2xlIjoiUk9MRV9VU0VSIn0.jZ4zWxq2NUNkY4APeYGzc2PX_JJqwmYESPChjvJaz4A
     Host: localhost:8080
    @@ -927,7 +928,7 @@
    POST /api/hubs/1/link-bundles HTTP/1.1
     Content-Type: application/json;charset=UTF-8
    -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzA4MDkwNzc0LCJzdWIiOiIxIiwiZXhwIjoxNzA4MDk0Mzc0LCJyb2xlIjoiUk9MRV9VU0VSIn0.VhlHhySUpcDTp7n5FzwbQFa5e8qqX2ANyR1ZtD3gIbs
    +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzA4MjY1MDI1LCJzdWIiOiIxIiwiZXhwIjoxNzA4MjY4NjI1LCJyb2xlIjoiUk9MRV9VU0VSIn0.jZ4zWxq2NUNkY4APeYGzc2PX_JJqwmYESPChjvJaz4A
     Content-Length: 53
     Host: localhost:8080
     
    @@ -1071,7 +1072,7 @@ 
    POST /api/links HTTP/1.1
     Content-Type: application/json;charset=UTF-8
    -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzA4MDkwNzc0LCJzdWIiOiIxIiwiZXhwIjoxNzA4MDk0Mzc0LCJyb2xlIjoiUk9MRV9VU0VSIn0.VhlHhySUpcDTp7n5FzwbQFa5e8qqX2ANyR1ZtD3gIbs
    +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzA4MjY1MDI1LCJzdWIiOiIxIiwiZXhwIjoxNzA4MjY4NjI1LCJyb2xlIjoiUk9MRV9VU0VSIn0.jZ4zWxq2NUNkY4APeYGzc2PX_JJqwmYESPChjvJaz4A
     Content-Length: 100
     Host: localhost:8080
     
    @@ -1193,7 +1194,7 @@ 
    GET /api/links?linkBundleId=1&page=0&size=10 HTTP/1.1
    -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzA4MDkwNzc0LCJzdWIiOiIxIiwiZXhwIjoxNzA4MDk0Mzc0LCJyb2xlIjoiUk9MRV9VU0VSIn0.VhlHhySUpcDTp7n5FzwbQFa5e8qqX2ANyR1ZtD3gIbs
    +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzA4MjY1MDI1LCJzdWIiOiIxIiwiZXhwIjoxNzA4MjY4NjI1LCJyb2xlIjoiUk9MRV9VU0VSIn0.jZ4zWxq2NUNkY4APeYGzc2PX_JJqwmYESPChjvJaz4A
     Host: localhost:8080
    @@ -1260,7 +1261,7 @@
    POST /api/hubs/1/links HTTP/1.1
     Content-Type: application/json;charset=UTF-8
    -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzA4MDkwNzc0LCJzdWIiOiIxIiwiZXhwIjoxNzA4MDk0Mzc0LCJyb2xlIjoiUk9MRV9VU0VSIn0.VhlHhySUpcDTp7n5FzwbQFa5e8qqX2ANyR1ZtD3gIbs
    +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzA4MjY1MDI1LCJzdWIiOiIxIiwiZXhwIjoxNzA4MjY4NjI1LCJyb2xlIjoiUk9MRV9VU0VSIn0.jZ4zWxq2NUNkY4APeYGzc2PX_JJqwmYESPChjvJaz4A
     Content-Length: 69
     Host: localhost:8080
     
    @@ -1470,6 +1471,128 @@ 
    +

    허브 링크 묶음 목록 조회

    +
    +

    request

    +
    +
    HTTP request
    +
    +
    +
    GET /api/hubs/1/link-bundles HTTP/1.1
    +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzA4MjY1MDI1LCJzdWIiOiIxIiwiZXhwIjoxNzA4MjY4NjI1LCJyb2xlIjoiUk9MRV9VU0VSIn0.jZ4zWxq2NUNkY4APeYGzc2PX_JJqwmYESPChjvJaz4A
    +Host: localhost:8080
    +
    +
    +
    +
    +
    Request headers
    + ++++ + + + + + + + + + + + + +
    NameDescription

    Authorization

    액세스 토큰

    +
    +
    +
    Path parameters
    + + ++++ + + + + + + + + + + + + +
    Table 1. /api/hubs/{hubId}/link-bundles
    ParameterDescription

    hubId

    허브 ID

    +
    +
    +
    +

    response

    +
    +
    HTTP response
    +
    +
    +
    HTTP/1.1 200 OK
    +Vary: Origin
    +Vary: Access-Control-Request-Method
    +Vary: Access-Control-Request-Headers
    +Content-Type: application/json;charset=UTF-8
    +Content-Length: 109
    +
    +{
    +  "linkBundles" : [ {
    +    "linkBundleId" : 1,
    +    "description" : "기본",
    +    "isDefault" : false
    +  } ]
    +}
    +
    +
    +
    +
    +
    Response fields
    + +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PathTypeDescription

    linkBundles

    Array

    링크 묶음 목록

    linkBundles[].linkBundleId

    Number

    링크 묶음 ID

    linkBundles[].description

    String

    링크 묶음 설명

    linkBundles[].isDefault

    Boolean

    기본 여부

    +
    +
    +
    @@ -1478,14 +1601,14 @@

    허브

    허브 생성

    -

    request

    +

    request

    -
    HTTP request
    +
    HTTP request
    POST /api/hubs HTTP/1.1
     Content-Type: application/json;charset=UTF-8
    -Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzA4MDkwNzc0LCJzdWIiOiIxIiwiZXhwIjoxNzA4MDk0Mzc0LCJyb2xlIjoiUk9MRV9VU0VSIn0.VhlHhySUpcDTp7n5FzwbQFa5e8qqX2ANyR1ZtD3gIbs
    +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzA4MjY1MDI1LCJzdWIiOiIxIiwiZXhwIjoxNzA4MjY4NjI1LCJyb2xlIjoiUk9MRV9VU0VSIn0.jZ4zWxq2NUNkY4APeYGzc2PX_JJqwmYESPChjvJaz4A
     Content-Length: 88
     Host: localhost:8080
     
    @@ -1498,7 +1621,7 @@ 
    -
    Request headers
    +
    Request headers
    @@ -1519,7 +1642,7 @@
    -
    Request fields
    +
    Request fields
    @@ -1554,9 +1677,9 @@
    -

    response

    +

    response

    -
    HTTP response
    +
    HTTP response
    HTTP/1.1 201 Created
    @@ -1573,7 +1696,7 @@ 
    -
    Response fields
    +
    Response fields
    @@ -1601,9 +1724,9 @@

    허브 목록 조회

    -

    request

    +

    request

    -
    HTTP request
    +
    HTTP request
    GET /api/hubs?page=0&size=0 HTTP/1.1
    @@ -1612,7 +1735,7 @@ 
    -
    Path parameters
    +
    Path parameters
    @@ -1639,9 +1762,9 @@
    -

    response

    +

    response

    -
    HTTP response
    +
    HTTP response
    HTTP/1.1 200 OK
    @@ -1666,7 +1789,7 @@ 
    -
    Response fields
    +
    Response fields
    Table 1. /api/hubs
    @@ -1732,7 +1855,7 @@
    diff --git a/src/test/java/com/seong/shoutlink/domain/hubmember/repository/StubHubMemberRepository.java b/src/test/java/com/seong/shoutlink/domain/hubmember/repository/StubHubMemberRepository.java new file mode 100644 index 0000000..bb7867a --- /dev/null +++ b/src/test/java/com/seong/shoutlink/domain/hubmember/repository/StubHubMemberRepository.java @@ -0,0 +1,31 @@ +package com.seong.shoutlink.domain.hubmember.repository; + +import com.seong.shoutlink.domain.hub.Hub; +import com.seong.shoutlink.domain.hubmember.service.HubMemberRepository; +import com.seong.shoutlink.domain.member.Member; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class StubHubMemberRepository implements HubMemberRepository { + + private final Map hubs = new HashMap<>(); + private final Map> hubMembers = new HashMap<>(); + + public void stub(Hub hub, Member... members) { + long nextKey = getNextKey(); + hubs.put(nextKey, hub); + hubMembers.put(nextKey, Arrays.asList(members)); + } + + private long getNextKey() { + return hubs.size() + 1; + } + + @Override + public boolean isHubMember(Hub hub, Member member) { + Long hubId = hub.getHubId(); + return hubMembers.get(hubId).contains(member); + } +} \ No newline at end of file diff --git a/src/test/java/com/seong/shoutlink/domain/linkbundle/controller/LinkBundleControllerTest.java b/src/test/java/com/seong/shoutlink/domain/linkbundle/controller/LinkBundleControllerTest.java index 732041f..86a3c58 100644 --- a/src/test/java/com/seong/shoutlink/domain/linkbundle/controller/LinkBundleControllerTest.java +++ b/src/test/java/com/seong/shoutlink/domain/linkbundle/controller/LinkBundleControllerTest.java @@ -124,4 +124,39 @@ void createHubLinkBundle() throws Exception { ) )); } + + @Test + @DisplayName("성공: 허브 링크 묶음 목록 조회 api 호출 시") + void findHubLinkBundles() throws Exception { + //given + long hubId = 1L; + FindLinkBundleResponse content = new FindLinkBundleResponse(1L, "기본", false); + FindLinkBundlesResponse response = new FindLinkBundlesResponse(List.of(content)); + + given(linkBundleService.findHubLinkBundles(any())).willReturn(response); + + //when + ResultActions resultActions = mockMvc.perform(get("/api/hubs/{hubId}/link-bundles", hubId) + .header(AUTHORIZATION, bearerAccessToken)); + + //then + resultActions.andExpect(status().isOk()) + .andDo(restDocs.document( + pathParameters( + parameterWithName("hubId").description("허브 ID") + ), + requestHeaders( + headerWithName(AUTHORIZATION).description("액세스 토큰").optional() + ), + responseFields( + fieldWithPath("linkBundles").type(JsonFieldType.ARRAY).description("링크 묶음 목록"), + fieldWithPath("linkBundles[].linkBundleId").type(JsonFieldType.NUMBER) + .description("링크 묶음 ID"), + fieldWithPath("linkBundles[].description").type(JsonFieldType.STRING) + .description("링크 묶음 설명"), + fieldWithPath("linkBundles[].isDefault").type(JsonFieldType.BOOLEAN) + .description("기본 여부") + ) + )); + } } \ No newline at end of file diff --git a/src/test/java/com/seong/shoutlink/domain/linkbundle/repository/FakeLinkBundleRepository.java b/src/test/java/com/seong/shoutlink/domain/linkbundle/repository/FakeLinkBundleRepository.java index 518aee2..f93d0ef 100644 --- a/src/test/java/com/seong/shoutlink/domain/linkbundle/repository/FakeLinkBundleRepository.java +++ b/src/test/java/com/seong/shoutlink/domain/linkbundle/repository/FakeLinkBundleRepository.java @@ -67,6 +67,11 @@ public Optional findHubLinkBundle(Long linkBundleId, Hub hub) { return memory.values().stream().findFirst(); } + @Override + public List findHubLinkBundles(Hub hubId) { + return memory.values().stream().toList(); + } + private long getNextId() { return memory.keySet().size() + 1; } diff --git a/src/test/java/com/seong/shoutlink/domain/linkbundle/service/LinkBundleServiceTest.java b/src/test/java/com/seong/shoutlink/domain/linkbundle/service/LinkBundleServiceTest.java index 73a7dd7..58cb60d 100644 --- a/src/test/java/com/seong/shoutlink/domain/linkbundle/service/LinkBundleServiceTest.java +++ b/src/test/java/com/seong/shoutlink/domain/linkbundle/service/LinkBundleServiceTest.java @@ -6,17 +6,21 @@ import com.seong.shoutlink.domain.exception.ErrorCode; import com.seong.shoutlink.domain.exception.ShoutLinkException; import com.seong.shoutlink.domain.hub.Hub; +import com.seong.shoutlink.domain.hubmember.repository.StubHubMemberRepository; import com.seong.shoutlink.domain.linkbundle.LinkBundle; import com.seong.shoutlink.domain.linkbundle.repository.FakeLinkBundleRepository; import com.seong.shoutlink.domain.linkbundle.service.request.CreateHubLinkBundleCommand; +import com.seong.shoutlink.domain.linkbundle.service.request.FindHubLinkBundlesCommand; import com.seong.shoutlink.domain.linkbundle.service.request.FindLinkBundlesCommand; import com.seong.shoutlink.domain.linkbundle.service.response.CreateLinkBundleCommand; import com.seong.shoutlink.domain.linkbundle.service.response.CreateLinkBundleResponse; import com.seong.shoutlink.domain.linkbundle.service.response.FindLinkBundleResponse; import com.seong.shoutlink.domain.linkbundle.service.response.FindLinkBundlesResponse; import com.seong.shoutlink.domain.member.Member; +import com.seong.shoutlink.domain.member.MemberRole; import com.seong.shoutlink.domain.member.repository.StubMemberRepository; import com.seong.shoutlink.fixture.HubFixture; +import com.seong.shoutlink.fixture.LinkBundleFixture; import com.seong.shoutlink.fixture.MemberFixture; import com.seong.shoutlink.fixture.StubHubRepository; import java.util.List; @@ -31,6 +35,7 @@ class LinkBundleServiceTest { private StubMemberRepository memberRepository; private FakeLinkBundleRepository linkBundleRepository; private StubHubRepository hubRepository; + private StubHubMemberRepository hubMemberRepository; @Nested @DisplayName("createLinkBundle 메서드 호출 시") @@ -44,8 +49,9 @@ void setUp() { memberRepository = new StubMemberRepository(savedMember); linkBundleRepository = new FakeLinkBundleRepository(); hubRepository = new StubHubRepository(); + hubMemberRepository = new StubHubMemberRepository(); linkBundleService = new LinkBundleService(memberRepository, hubRepository, - linkBundleRepository); + hubMemberRepository, linkBundleRepository); } @Test @@ -96,8 +102,9 @@ void setUp() { memberRepository = new StubMemberRepository(); linkBundleRepository = new FakeLinkBundleRepository(); hubRepository = new StubHubRepository(); + hubMemberRepository = new StubHubMemberRepository(); linkBundleService = new LinkBundleService(memberRepository, hubRepository, - linkBundleRepository); + hubMemberRepository, linkBundleRepository); } @Test @@ -135,8 +142,9 @@ void setUp() { memberRepository = new StubMemberRepository(); hubRepository = new StubHubRepository(); linkBundleRepository = new FakeLinkBundleRepository(); + hubMemberRepository = new StubHubMemberRepository(); linkBundleService = new LinkBundleService(memberRepository, hubRepository, - linkBundleRepository); + hubMemberRepository, linkBundleRepository); } @Test @@ -202,4 +210,141 @@ void unauthorized_WhenMemberIsNotHubMaster() { .isEqualTo(ErrorCode.UNAUTHORIZED); } } + + @Nested + @DisplayName("findHubLinkBundles 메서드 호출 시") + class FindHubLinkBundlesTest { + + @BeforeEach + void setUp() { + memberRepository = new StubMemberRepository(); + hubRepository = new StubHubRepository(); + linkBundleRepository = new FakeLinkBundleRepository(); + hubMemberRepository = new StubHubMemberRepository(); + linkBundleService = new LinkBundleService(memberRepository, hubRepository, + hubMemberRepository, linkBundleRepository); + } + + @Test + @DisplayName("성공: 허브 링크 묶음 목록 조회됨") + void findHubLinkBundles() { + //given + Member member = MemberFixture.member(); + Hub hub = HubFixture.hub(member); + LinkBundle linkBundle = LinkBundleFixture.linkBundle(); + memberRepository.stub(member); + hubRepository.stub(hub); + linkBundleRepository.stub(linkBundle); + FindHubLinkBundlesCommand command = new FindHubLinkBundlesCommand(1L, + member.getMemberId()); + + //when + FindLinkBundlesResponse response = linkBundleService.findHubLinkBundles(command); + + //then + assertThat(response.linkBundles()).hasSize(1) + .allSatisfy(findLinkBundle -> { + assertThat(findLinkBundle.linkBundleId()) + .isEqualTo(linkBundle.getLinkBundleId()); + assertThat(findLinkBundle.description()).isEqualTo(linkBundle.getDescription()); + assertThat(findLinkBundle.isDefault()).isEqualTo(linkBundle.isDefault()); + }); + } + + @Test + @DisplayName("예외(NotFound): 존재하지 않는 허브") + void notFound_WhenHubNotFound() { + //given + FindHubLinkBundlesCommand command = new FindHubLinkBundlesCommand(1L, 1L); + + //when + Exception exception = catchException( + () -> linkBundleService.findHubLinkBundles(command)); + + //then + assertThat(exception).isInstanceOf(ShoutLinkException.class) + .extracting(e -> ((ShoutLinkException) e).getErrorCode()) + .isEqualTo(ErrorCode.NOT_FOUND); + } + + @Nested + @DisplayName("비공개 허브인 경우") + class WhenHubIsPrivate { + + @Test + @DisplayName("성공: 사용자가 허브 소속일 때 허브 링크 목록 조회됨") + void findHubLinkBundles_WhenMemberIsHubMember() { + //given + Member member = MemberFixture.member(); + Hub hub = HubFixture.privateHub(member); + LinkBundle linkBundle = LinkBundleFixture.linkBundle(); + memberRepository.stub(member); + hubRepository.stub(hub); + linkBundleRepository.stub(linkBundle); + hubMemberRepository.stub(hub, member); + FindHubLinkBundlesCommand command = new FindHubLinkBundlesCommand(1L, + member.getMemberId()); + + //when + FindLinkBundlesResponse response = linkBundleService.findHubLinkBundles(command); + + //then + assertThat(response.linkBundles()).hasSize(1) + .allSatisfy(findLinkBundle -> { + assertThat(findLinkBundle.linkBundleId()).isEqualTo( + linkBundle.getLinkBundleId()); + assertThat(findLinkBundle.description()).isEqualTo( + linkBundle.getDescription()); + assertThat(findLinkBundle.isDefault()).isEqualTo(linkBundle.isDefault()); + }); + } + + @Test + @DisplayName("예외(Unauthenticated): 인증되지 않은 사용자") + void unauthenticated_WhenMemberIsUnauthenticated() { + //given + Member member = MemberFixture.member(); + Hub hub = HubFixture.privateHub(member); + memberRepository.stub(member); + hubRepository.stub(hub); + FindHubLinkBundlesCommand command = new FindHubLinkBundlesCommand(hub.getHubId(), + null); + + //when + Exception exception = catchException( + () -> linkBundleService.findHubLinkBundles(command)); + + //then + assertThat(exception).isInstanceOf(ShoutLinkException.class) + .extracting(e -> ((ShoutLinkException) e).getErrorCode()) + .isEqualTo(ErrorCode.UNAUTHENTICATED); + } + + @Test + @DisplayName("예외(Unauthorized): 사용자가 허브 소속이 아닐 때") + void unauthorized_WhenMemberIsNotHubMember() { + //given + Member member = MemberFixture.member(); + Hub hub = HubFixture.privateHub(member); + memberRepository.stub(member); + hubRepository.stub(hub); + hubMemberRepository.stub(hub, member); + + Member anotherMember = new Member(member.getMemberId() + 1, "email@email.com", + "asdf123!", "nickname", MemberRole.ROLE_USER); + memberRepository.stub(anotherMember); + FindHubLinkBundlesCommand command = new FindHubLinkBundlesCommand(hub.getHubId(), + anotherMember.getMemberId()); + + //when + Exception exception = catchException( + () -> linkBundleService.findHubLinkBundles(command)); + + //then + assertThat(exception).isInstanceOf(ShoutLinkException.class) + .extracting(e -> ((ShoutLinkException) e).getErrorCode()) + .isEqualTo(ErrorCode.UNAUTHORIZED); + } + } + } } diff --git a/src/test/java/com/seong/shoutlink/fixture/HubFixture.java b/src/test/java/com/seong/shoutlink/fixture/HubFixture.java index cf68995..9638d9a 100644 --- a/src/test/java/com/seong/shoutlink/fixture/HubFixture.java +++ b/src/test/java/com/seong/shoutlink/fixture/HubFixture.java @@ -15,6 +15,10 @@ public static Hub hub(Member member) { return new Hub(HUB_ID, member.getMemberId(), NAME, DESCRIPTION, IS_PRIVATE); } + public static Hub privateHub(Member member) { + return new Hub(HUB_ID, member.getMemberId(), NAME, DESCRIPTION, true); + } + public static HubWithMaster hubWithMaster(Member member) { return new HubWithMaster(hub(member), member); } diff --git a/src/test/java/com/seong/shoutlink/global/auth/AuthTestController.java b/src/test/java/com/seong/shoutlink/global/auth/AuthTestController.java index f0141c7..74edf69 100644 --- a/src/test/java/com/seong/shoutlink/global/auth/AuthTestController.java +++ b/src/test/java/com/seong/shoutlink/global/auth/AuthTestController.java @@ -1,6 +1,7 @@ package com.seong.shoutlink.global.auth; import com.seong.shoutlink.domain.auth.LoginUser; +import com.seong.shoutlink.domain.auth.NullableUser; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -14,4 +15,9 @@ public class AuthTestController { public ResponseEntity loginUser(@LoginUser Long memberId) { return ResponseEntity.ok(memberId); } + + @GetMapping("/nullable-user") + public ResponseEntity nullableUser(@NullableUser Long memberId) { + return ResponseEntity.ok(memberId); + } } diff --git a/src/test/java/com/seong/shoutlink/global/auth/resolver/NullableUserArgumentResolverTest.java b/src/test/java/com/seong/shoutlink/global/auth/resolver/NullableUserArgumentResolverTest.java new file mode 100644 index 0000000..568504a --- /dev/null +++ b/src/test/java/com/seong/shoutlink/global/auth/resolver/NullableUserArgumentResolverTest.java @@ -0,0 +1,45 @@ +package com.seong.shoutlink.global.auth.resolver; + +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.seong.shoutlink.base.BaseControllerTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.test.web.servlet.ResultActions; + +class NullableUserArgumentResolverTest extends BaseControllerTest { + + @Nested + @DisplayName("@NullableUser를 핸들러 메서드의 파라미터에 선언 시") + class LoginUserTest { + + @Test + @DisplayName("성공: 인증된 사용자의 principal을 반환") + void returnAuthenticatedUserPrincipal() throws Exception { + //given + + //when + ResultActions resultActions = mockMvc.perform(get("/api/test/nullable-user") + .header(AUTHORIZATION, bearerAccessToken)); + + //then + resultActions.andExpect(status().isOk()) + .andExpect(jsonPath("$").value(1)); + } + + @Test + @DisplayName("성공: 인증되지 않은 사용자 요청인 경우 무시") + void ignore_WhenUnauthenticatedUserRequest() throws Exception { + //given + + //when + ResultActions resultActions = mockMvc.perform(get("/api/test/nullable-user")); + + //then + resultActions.andExpect(status().isOk()); + } + } +} \ No newline at end of file