diff --git a/build.gradle b/build.gradle index b11f3cfa..d691e0c9 100644 --- a/build.gradle +++ b/build.gradle @@ -66,6 +66,7 @@ dependencies { annotationProcessor "jakarta.persistence:jakarta.persistence-api" testImplementation 'org.springframework.cloud:spring-cloud-contract-stub-runner' + testImplementation 'org.testcontainers:localstack' } diff --git a/src/docs/asciidoc/index.adoc b/src/docs/asciidoc/index.adoc index c2a5685c..dc6be2fd 100644 --- a/src/docs/asciidoc/index.adoc +++ b/src/docs/asciidoc/index.adoc @@ -9,24 +9,26 @@ endif::[] = Stumeet API 문서 -== 소셜 로그인 +== 회원 관리 + +=== 소셜 로그인 소셜 로그인 시 사용되는 API입니다. 현재 카카오, 애플 로그인이 가능합니다. -=== POST /api/v1/oauth +==== POST /api/v1/oauth -==== 요청 +===== 요청 .카카오 로그인 include::{snippets}/social_login/success/http-request.adoc[] include::{snippets}/social_login/success/request-headers.adoc[] -==== 응답 성공 (200) +===== 응답 성공 (200) include::{snippets}/social_login/success/response-body.adoc[] include::{snippets}/social_login/success/response-fields.adoc[] -==== 응답 실패 (401) +===== 응답 실패 (401) .유효한 토큰이 아닌 경우 include::{snippets}/social_login/fail/invalid-token/response-body.adoc[] @@ -35,38 +37,73 @@ include::{snippets}/social_login/fail/not-exist-header/response-body.adoc[] include::{snippets}/social_login/fail/invalid-token/response-fields.adoc[] -== 로그아웃 +=== 로그아웃 로그아웃 시 사용되는 API입니다. -=== POST /api/v1/logout +==== POST /api/v1/logout -==== 요청 +===== 요청 include::{snippets}/logout/success/http-request.adoc[] include::{snippets}/logout/success/request-headers.adoc[] -==== 응답 성공 (200) +===== 응답 성공 (200) include::{snippets}/logout/success/response-body.adoc[] include::{snippets}/logout/success/response-fields.adoc[] -== 토큰 재발급 + +=== 최초 로그인시 회원가입 + +최초 로그인 시 회원가입을 위해 사용되는 API입니다. + +==== POST /api/v1/signup + +===== 요청 +include::{snippets}/signup/success/http-request.adoc[] +include::{snippets}/signup/success/request-headers.adoc[] +include::{snippets}/signup/success/request-parts.adoc[] +include::{snippets}/signup/success/query-parameters.adoc[] + +===== 응답 성공 (201) +include::{snippets}/signup/success/response-body.adoc[] +include::{snippets}/signup/success/response-fields.adoc[] + +===== 응답 실패 (400) +.회원가입 요청 값이 유효하지 않은 경우 +include::{snippets}/signup/fail/invalid-request/response-body.adoc[] +include::{snippets}/signup/fail/invalid-request/response-fields.adoc[] + +.전달한 분야 정보가 존재하지 않는 분야인 경우 +include::{snippets}/signup/fail/not-exists-profession/response-body.adoc[] +include::{snippets}/signup/fail/not-exists-profession/response-fields.adoc[] + +.전달한 이미지가 유효하지 않은 경우 +include::{snippets}/signup/fail/invalid-image/response-body.adoc[] +include::{snippets}/signup/fail/invalid-image/response-fields.adoc[] + +===== 응답 실패 (403) +.이미 가입된 회원인 경우 +include::{snippets}/signup/fail/already-signup-member/response-body.adoc[] +include::{snippets}/signup/fail/already-signup-member/response-fields.adoc[] + +=== 토큰 재발급 서버로 부터 받은 액세스 토큰이 만료된 경우 액세스 토큰 재발급을 위해 사용되는 API입니다. -=== POST /api/v1/tokens +==== POST /api/v1/tokens -==== 요청 +===== 요청 include::{snippets}/token_renew/success/http-request.adoc[] include::{snippets}/token_renew/success/request-body.adoc[] include::{snippets}/token_renew/success/request-fields.adoc[] -==== 응답 성공 (200) +===== 응답 성공 (200) include::{snippets}/token_renew/success/response-body.adoc[] include::{snippets}/token_renew/success/response-fields.adoc[] -==== 응답 실패(400) +===== 응답 실패(400) .액세스 토큰과 매칭되는 리프레시 토큰이 없는 경우 include::{snippets}/token_renew/fail/not-match-access-token/response-body.adoc[] include::{snippets}/token_renew/fail/not-match-access-token/response-fields.adoc[] @@ -84,20 +121,20 @@ include::{snippets}/token_renew/fail/expired-refresh-token/response-body.adoc[] include::{snippets}/token_renew/fail/expired-refresh-token/response-fields.adoc[] -== 사용자 닉네임 유효성 검사 +=== 사용자 닉네임 유효성 검사 사용자 닉네임의 유효성을 검사하는 API입니다. -=== GET /api/v1/members/validate-nickname +==== GET /api/v1/members/validate-nickname -==== 요청 +===== 요청 include::{snippets}/validate_nickname/success/http-request.adoc[] include::{snippets}/validate_nickname/success/request-headers.adoc[] include::{snippets}/validate_nickname/success/query-parameters.adoc[] -==== 응답 성공 (200) +===== 응답 성공 (200) -==== 응답 실패 (400) +===== 응답 실패 (400) .닉네임이 유효성 검사에 실패한 경우 include::{snippets}/validate_nickname/fail/invalid/response-body.adoc[] @@ -107,17 +144,18 @@ include::{snippets}/validate_nickname/fail/invalid/response-fields.adoc[] include::{snippets}/validate_nickname/fail/duplicate/response-body.adoc[] include::{snippets}/validate_nickname/fail/duplicate/response-fields.adoc[] -== 분야 정보 전체 조회 +== 분야 관리 +=== 분야 정보 전체 조회 모든 분야 정보를 조회하는 API입니다. -=== GET /api/v1/professions +==== GET /api/v1/professions -==== 요청 +===== 요청 include::{snippets}/find_professions/success/http-request.adoc[] include::{snippets}/find_professions/success/request-headers.adoc[] -==== 응답 성공 (200) +===== 응답 성공 (200) include::{snippets}/find_professions/success/response-body.adoc[] include::{snippets}/find_professions/success/response-fields.adoc[] diff --git a/src/main/java/com/stumeet/server/common/auth/handler/ForbiddenAccessHandler.java b/src/main/java/com/stumeet/server/common/auth/handler/ForbiddenAccessHandler.java index 87c6dd77..ddd1cdca 100644 --- a/src/main/java/com/stumeet/server/common/auth/handler/ForbiddenAccessHandler.java +++ b/src/main/java/com/stumeet/server/common/auth/handler/ForbiddenAccessHandler.java @@ -2,12 +2,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.stumeet.server.common.model.ApiResponse; +import com.stumeet.server.common.response.ErrorCode; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; @@ -24,9 +24,10 @@ public class ForbiddenAccessHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { log.warn(accessDeniedException.getMessage()); + log.debug("", accessDeniedException); response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setContentType("application/json;charset=utf-8"); - response.getWriter().write(objectMapper.writeValueAsString(ApiResponse.fail(HttpStatus.FORBIDDEN.value(), "유효하지 않은 요청입니다."))); + response.getWriter().write(objectMapper.writeValueAsString(ApiResponse.fail(ErrorCode.ACCESS_DENIED_EXCEPTION))); } } diff --git a/src/main/java/com/stumeet/server/common/config/AwsS3Config.java b/src/main/java/com/stumeet/server/common/config/AwsS3Config.java index feb8b033..74a058b8 100644 --- a/src/main/java/com/stumeet/server/common/config/AwsS3Config.java +++ b/src/main/java/com/stumeet/server/common/config/AwsS3Config.java @@ -3,40 +3,39 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; - -import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Profile; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; -import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; @Configuration -@RequiredArgsConstructor +@Profile("!test") public class AwsS3Config { - @Value("${spring.cloud.config.server.awss3.region}") - private String region; + @Value("${spring.cloud.config.server.awss3.region}") + private String region; - @Value("${spring.cloud.config.server.awss3.credentials.access-key}") - private String accessKey; + @Value("${spring.cloud.config.server.awss3.credentials.access-key}") + private String accessKey; - @Value("${spring.cloud.config.server.awss3.credentials.secret-key}") - private String secretKey; + @Value("${spring.cloud.config.server.awss3.credentials.secret-key}") + private String secretKey; - private AwsBasicCredentials awsBasicCredentials() { - return AwsBasicCredentials.create(accessKey, secretKey); - } + private AwsBasicCredentials awsBasicCredentials() { + return AwsBasicCredentials.create(accessKey, secretKey); + } - @Bean - public StaticCredentialsProvider staticCredentialsProvider() { - return StaticCredentialsProvider.create(awsBasicCredentials()); - } + @Bean + public StaticCredentialsProvider staticCredentialsProvider() { + return StaticCredentialsProvider.create(awsBasicCredentials()); + } - @Bean - public S3Client s3Client() { - return S3Client.builder() - .credentialsProvider(staticCredentialsProvider()) - .region(Region.of(region)) - .build(); - } + @Bean + public S3Client s3Client() { + return S3Client.builder() + .credentialsProvider(staticCredentialsProvider()) + .region(Region.of(region)) + .build(); + } } diff --git a/src/main/java/com/stumeet/server/common/response/ErrorCode.java b/src/main/java/com/stumeet/server/common/response/ErrorCode.java index b37346d9..6226136a 100644 --- a/src/main/java/com/stumeet/server/common/response/ErrorCode.java +++ b/src/main/java/com/stumeet/server/common/response/ErrorCode.java @@ -25,6 +25,11 @@ public enum ErrorCode { NOT_MATCHED_REFRESH_TOKEN_EXCEPTION(HttpStatus.BAD_REQUEST, "요청으로 전달한 리프레시 토큰과 서버의 리프레시 토큰이 일치하지 않습니다."), EXPIRED_REFRESH_TOKEN_EXCEPTION(HttpStatus.BAD_REQUEST, "리프레시 토큰이 만료되었습니다."), + /* + 403 - FORBIDDEN + */ + ACCESS_DENIED_EXCEPTION(HttpStatus.FORBIDDEN, "유효하지 않은 요청입니다."), + /* 500 - INTERNAL SERVER ERROR */ diff --git a/src/main/java/com/stumeet/server/member/adapter/in/web/MemberAuthApi.java b/src/main/java/com/stumeet/server/member/adapter/in/web/MemberAuthApi.java index 2d72f79f..58aa855a 100644 --- a/src/main/java/com/stumeet/server/member/adapter/in/web/MemberAuthApi.java +++ b/src/main/java/com/stumeet/server/member/adapter/in/web/MemberAuthApi.java @@ -31,8 +31,8 @@ public ResponseEntity> signup( memberAuthUseCase.signup(loginMember.getMember(), request); return new ResponseEntity<>( - ApiResponse.success(HttpStatus.CREATED.value(), "회원가입에 성공했습니다."), - HttpStatus.CREATED + ApiResponse.success(HttpStatus.OK.value(), "회원가입에 성공했습니다."), + HttpStatus.OK ); } diff --git a/src/main/java/com/stumeet/server/member/application/port/in/MemberSignupCommand.java b/src/main/java/com/stumeet/server/member/application/port/in/MemberSignupCommand.java index 8ac487ed..ab2ce88a 100644 --- a/src/main/java/com/stumeet/server/member/application/port/in/MemberSignupCommand.java +++ b/src/main/java/com/stumeet/server/member/application/port/in/MemberSignupCommand.java @@ -7,7 +7,7 @@ public record MemberSignupCommand( - @NotNull + @NotNull(message = "이미지를 첨부해주세요") MultipartFile image, @Size(min = 2, max = 10, message = "닉네임을 2 ~ 10자로 입력해주세요") diff --git a/src/test/java/com/stumeet/server/helper/TestAwsS3Config.java b/src/test/java/com/stumeet/server/helper/TestAwsS3Config.java new file mode 100644 index 00000000..921b2abf --- /dev/null +++ b/src/test/java/com/stumeet/server/helper/TestAwsS3Config.java @@ -0,0 +1,43 @@ +package com.stumeet.server.helper; + +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.testcontainers.containers.localstack.LocalStackContainer; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; + +@TestConfiguration +@Testcontainers +public class TestAwsS3Config implements AfterAllCallback { + protected static final DockerImageName LOCALSTACK_CONTAINER_VERSION = DockerImageName.parse("localstack/localstack:3.2"); + + private static final LocalStackContainer LOCALSTACK_CONTAINER = new LocalStackContainer(LOCALSTACK_CONTAINER_VERSION) + .withServices(LocalStackContainer.Service.S3); + + static { + LOCALSTACK_CONTAINER.start(); + } + + @Bean + public S3Client s3Client() { + S3Client s3Client = S3Client.builder() + .endpointOverride(LOCALSTACK_CONTAINER.getEndpoint()) + .region(Region.of(LOCALSTACK_CONTAINER.getRegion())) + .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create(LOCALSTACK_CONTAINER.getAccessKey(), LOCALSTACK_CONTAINER.getSecretKey()))) + .build(); + s3Client.createBucket(b -> b.bucket("stumeet")); + return s3Client; + } + + @Override + public void afterAll(ExtensionContext context) throws Exception { +// LOCALSTACK_CONTAINER.stop(); + } + +} diff --git a/src/test/java/com/stumeet/server/helper/WithMockMember.java b/src/test/java/com/stumeet/server/helper/WithMockMember.java new file mode 100644 index 00000000..e3c4dc79 --- /dev/null +++ b/src/test/java/com/stumeet/server/helper/WithMockMember.java @@ -0,0 +1,15 @@ +package com.stumeet.server.helper; + +import com.stumeet.server.member.domain.UserRole; +import org.springframework.security.test.context.support.WithSecurityContext; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + + +@Retention(RetentionPolicy.RUNTIME) +@WithSecurityContext(factory = WithMockMemberSecurityContextFactory.class) +public @interface WithMockMember { + UserRole authority() default UserRole.MEMBER; + +} diff --git a/src/test/java/com/stumeet/server/helper/WithMockMemberSecurityContextFactory.java b/src/test/java/com/stumeet/server/helper/WithMockMemberSecurityContextFactory.java new file mode 100644 index 00000000..87d79efb --- /dev/null +++ b/src/test/java/com/stumeet/server/helper/WithMockMemberSecurityContextFactory.java @@ -0,0 +1,32 @@ +package com.stumeet.server.helper; + +import com.stumeet.server.common.auth.model.LoginMember; +import com.stumeet.server.common.auth.token.StumeetAuthenticationToken; +import com.stumeet.server.member.domain.Member; +import com.stumeet.server.member.domain.OAuthProvider; +import com.stumeet.server.stub.MemberStub; +import com.stumeet.server.stub.TokenStub; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.test.context.support.WithSecurityContextFactory; + +public class WithMockMemberSecurityContextFactory implements WithSecurityContextFactory { + + @Override + public SecurityContext createSecurityContext(WithMockMember annotation) { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + + Member member = MemberStub.getMember(annotation); + LoginMember loginMember = new LoginMember(member); + Authentication token = StumeetAuthenticationToken.authenticateOAuth( + loginMember.getAuthorities(), + TokenStub.getMockAccessToken(), + "refreshToken", + OAuthProvider.KAKAO.getProvider(), + loginMember + ); + context.setAuthentication(token); + return context; + } +} diff --git a/src/test/java/com/stumeet/server/member/adapter/in/web/MemberAuthApiTest.java b/src/test/java/com/stumeet/server/member/adapter/in/web/MemberAuthApiTest.java index a15897ac..622708e6 100644 --- a/src/test/java/com/stumeet/server/member/adapter/in/web/MemberAuthApiTest.java +++ b/src/test/java/com/stumeet/server/member/adapter/in/web/MemberAuthApiTest.java @@ -1,9 +1,12 @@ package com.stumeet.server.member.adapter.in.web; import com.stumeet.server.common.token.JwtTokenProvider; +import com.stumeet.server.helper.WithMockMember; import com.stumeet.server.member.adapter.out.persistence.JpaMemberRepository; import com.stumeet.server.member.adapter.out.persistence.MemberJpaEntity; +import com.stumeet.server.member.application.port.in.MemberSignupCommand; import com.stumeet.server.member.application.port.in.TokenRenewCommand; +import com.stumeet.server.member.domain.UserRole; import com.stumeet.server.stub.MemberStub; import com.stumeet.server.stub.TokenStub; import com.stumeet.server.template.ApiTest; @@ -12,15 +15,22 @@ import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.transaction.annotation.Transactional; import org.testcontainers.containers.GenericContainer; import org.testcontainers.junit.jupiter.Container; +import java.io.InputStream; + +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.mockmvc.RestDocumentationRequestBuilders.multipart; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.payload.PayloadDocumentation.*; +import static org.springframework.restdocs.request.RequestDocumentation.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -184,4 +194,185 @@ void expiredRefreshTokenTest() throws Exception { } } + @Nested + @DisplayName("최초 로그인시 회원가입") + class Signup { + private final String path = "/api/v1/signup"; + + @Test + @WithMockMember(authority = UserRole.FIRST_LOGIN) + @DisplayName("[성공] 최초 로그인시 회원가입에 성공합니다.") + void successTest() throws Exception { + MemberSignupCommand request = MemberStub.getMemberSignupCommand(); + + mockMvc.perform(multipart(path) + .file((MockMultipartFile) request.image()) + .header("Authorization", TokenStub.getMockAccessToken()) + .queryParam("nickname", request.nickname()) + .queryParam("region", request.region()) + .queryParam("profession", String.valueOf(request.profession())) + .contentType(MediaType.MULTIPART_FORM_DATA) + ).andExpect(status().isOk()) + .andDo(document("signup/success", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + queryParameters( + parameterWithName("nickname").description("회원 닉네임"), + parameterWithName("region").description("회원 지역"), + parameterWithName("profession").description("회원 분야") + ), + requestParts( + partWithName("image").description("회원 프로필 이미지") + ), + requestHeaders( + headerWithName("Authorization").description("서버로부터 전달받은 액세스 토큰") + ), + responseFields( + fieldWithPath("code").type(JsonFieldType.NUMBER).description("응답에 대한 결과 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("응답에 대한 메시지") + ) + )); + } + + @Test + @WithMockMember + @DisplayName("[실패] 이미 가입된 회원이 회원가입을 시도하는 경우 실패합니다.") + void alreadySignupMemberFailTest() throws Exception { + MemberSignupCommand request = MemberStub.getMemberSignupCommand(); + + mockMvc.perform(multipart(path) + .file((MockMultipartFile) request.image()) + .header("Authorization", TokenStub.getMockAccessToken()) + .queryParam("nickname", request.nickname()) + .queryParam("region", request.region()) + .queryParam("profession", String.valueOf(request.profession())) + .contentType(MediaType.MULTIPART_FORM_DATA) + ).andExpect(status().isForbidden()) + .andDo(document("signup/fail/already-signup-member", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + queryParameters( + parameterWithName("nickname").description("회원 닉네임"), + parameterWithName("region").description("회원 지역"), + parameterWithName("profession").description("회원 분야") + ), + requestParts( + partWithName("image").description("회원 프로필 이미지") + ), + requestHeaders( + headerWithName("Authorization").description("서버로부터 전달받은 액세스 토큰") + ), + responseFields( + fieldWithPath("code").type(JsonFieldType.NUMBER).description("응답에 대한 결과 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("응답에 대한 메시지") + ) + )); + } + + @Test + @WithMockMember(authority = UserRole.FIRST_LOGIN) + @DisplayName("[실패] 회원가입 요청 값이 유효한 값이 아니면 회원가입에 실패합니다.") + void invalidRequestTest() throws Exception { + mockMvc.perform(multipart(path) + .file(new MockMultipartFile("1", (InputStream) null)) + .header("Authorization", TokenStub.getMockAccessToken()) + .queryParam("nickname", "") + .queryParam("region", "") + .queryParam("profession", "") + .contentType(MediaType.MULTIPART_FORM_DATA)) + .andExpect(status().isBadRequest()) + .andDo(document("signup/fail/invalid-request", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + queryParameters( + parameterWithName("nickname").description("회원 닉네임"), + parameterWithName("region").description("회원 지역"), + parameterWithName("profession").description("회원 분야") + ), + requestHeaders( + headerWithName("Authorization").description("서버로부터 전달받은 액세스 토큰") + ), + responseFields( + fieldWithPath("code").type(JsonFieldType.NUMBER).description("응답에 대한 결과 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("응답에 대한 메시지"), + fieldWithPath("data[].message").type(JsonFieldType.STRING).description("요청 실패 사유에 대한 메시지") + ) + )); + } + + @Test + @WithMockMember(authority = UserRole.FIRST_LOGIN) + @DisplayName("[실패] 전달한 분야 정보가 존재하지 않으면 회원가입에 실패합니다.") + void notExistsProfessionTest() throws Exception { + MemberSignupCommand request = MemberStub.getMemberSignupCommand(); + String notExistsProfession = "0"; + + mockMvc.perform(multipart(path) + .file((MockMultipartFile) request.image()) + .header("Authorization", TokenStub.getMockAccessToken()) + .queryParam("nickname", request.nickname()) + .queryParam("region", request.region()) + .queryParam("profession", notExistsProfession) + .contentType(MediaType.MULTIPART_FORM_DATA) + ).andExpect(status().isBadRequest()) + .andDo(document("signup/fail/not-exists-profession", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + queryParameters( + parameterWithName("nickname").description("회원 닉네임"), + parameterWithName("region").description("회원 지역"), + parameterWithName("profession").description("회원 분야") + ), + requestParts( + partWithName("image").description("회원 프로필 이미지") + ), + requestHeaders( + headerWithName("Authorization").description("서버로부터 전달받은 액세스 토큰") + ), + responseFields( + fieldWithPath("code").type(JsonFieldType.NUMBER).description("응답에 대한 결과 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("응답에 대한 메시지") + ) + )); + } + + @Test + @WithMockMember(authority = UserRole.FIRST_LOGIN) + @DisplayName("[실패] 전달한 이미지가 유효한 이미지가 아니면 회원가입에 실패합니다.") + void invalidImageTest() throws Exception { + MemberSignupCommand request = MemberStub.getMemberSignupCommand(); + MockMultipartFile invalidImage = new MockMultipartFile("image", "test.jpa", "plain/text", "test".getBytes()); + + mockMvc.perform(multipart(path) + .file(invalidImage) + .header("Authorization", TokenStub.getMockAccessToken()) + .queryParam("nickname", request.nickname()) + .queryParam("region", request.region()) + .queryParam("profession", String.valueOf(request.profession())) + .contentType(MediaType.MULTIPART_FORM_DATA) + ).andExpect(status().isBadRequest()) + .andDo(document("signup/fail/invalid-image", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + queryParameters( + parameterWithName("nickname").description("회원 닉네임"), + parameterWithName("region").description("회원 지역"), + parameterWithName("profession").description("회원 분야") + ), + requestParts( + partWithName("image").description("회원 프로필 이미지") + ), + requestHeaders( + headerWithName("Authorization").description("서버로부터 전달받은 액세스 토큰") + ), + responseFields( + fieldWithPath("code").type(JsonFieldType.NUMBER).description("응답에 대한 결과 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("응답에 대한 메시지") + ) + )); + + } + } + + } \ No newline at end of file diff --git a/src/test/java/com/stumeet/server/member/adapter/in/web/MemberValidApiTest.java b/src/test/java/com/stumeet/server/member/adapter/in/web/MemberValidApiTest.java index 18a442f8..b53f8a2c 100644 --- a/src/test/java/com/stumeet/server/member/adapter/in/web/MemberValidApiTest.java +++ b/src/test/java/com/stumeet/server/member/adapter/in/web/MemberValidApiTest.java @@ -6,12 +6,12 @@ import com.stumeet.server.stub.MemberStub; import com.stumeet.server.stub.TokenStub; import com.stumeet.server.template.ApiTest; +import com.stumeet.server.helper.WithMockMember; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.test.context.support.WithMockUser; import org.springframework.transaction.annotation.Transactional; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; @@ -45,7 +45,7 @@ void setUp() { } @Test - @WithMockUser + @WithMockMember @DisplayName("[성공] 유효한 닉네임을 입력하면 중복 검증에 통과한다.") void successTest() throws Exception { String nickname = "닉네임"; @@ -69,7 +69,7 @@ void successTest() throws Exception { } @Test - @WithMockUser + @WithMockMember @DisplayName("[실패] 유효하지 않은 닉네임을 입력하면 검증에 실패합니다.") void invalidRequestTest() throws Exception { String nickname = "닉"; @@ -93,7 +93,7 @@ void invalidRequestTest() throws Exception { } @Test - @WithMockUser + @WithMockMember @DisplayName("[실패] 닉네임이 중복되면 검증에 실패합니다.") void duplicateNicknameTest() throws Exception { String nickname = member.getName(); diff --git a/src/test/java/com/stumeet/server/profession/adapter/in/web/ProfessionQueryApiTest.java b/src/test/java/com/stumeet/server/profession/adapter/in/web/ProfessionQueryApiTest.java index 83925517..0bf7a58c 100644 --- a/src/test/java/com/stumeet/server/profession/adapter/in/web/ProfessionQueryApiTest.java +++ b/src/test/java/com/stumeet/server/profession/adapter/in/web/ProfessionQueryApiTest.java @@ -3,11 +3,11 @@ import com.stumeet.server.common.auth.model.AuthenticationHeader; import com.stumeet.server.stub.TokenStub; import com.stumeet.server.template.ApiTest; +import com.stumeet.server.helper.WithMockMember; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.restdocs.payload.JsonFieldType; -import org.springframework.security.test.context.support.WithMockUser; import org.springframework.transaction.annotation.Transactional; import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; @@ -30,7 +30,7 @@ class GetProfessions { private final String path = "/api/v1/professions"; @Test - @WithMockUser + @WithMockMember @DisplayName("[성공] 분야 정보를 전체 조회할 수 있다.") void successTest() throws Exception{ mockMvc.perform(get(path) diff --git a/src/test/java/com/stumeet/server/stub/MemberStub.java b/src/test/java/com/stumeet/server/stub/MemberStub.java index aefa7cae..6a51e41b 100644 --- a/src/test/java/com/stumeet/server/stub/MemberStub.java +++ b/src/test/java/com/stumeet/server/stub/MemberStub.java @@ -1,8 +1,12 @@ package com.stumeet.server.stub; import com.stumeet.server.member.adapter.out.persistence.MemberJpaEntity; +import com.stumeet.server.member.application.port.in.MemberSignupCommand; import com.stumeet.server.member.domain.AuthType; +import com.stumeet.server.member.domain.Member; import com.stumeet.server.member.domain.UserRole; +import com.stumeet.server.helper.WithMockMember; +import org.springframework.mock.web.MockMultipartFile; public class MemberStub { @@ -29,4 +33,22 @@ public static MemberJpaEntity getMemberEntity() { .sugarContents(0.0) .build(); } + + public static MemberSignupCommand getMemberSignupCommand() { + MockMultipartFile image = new MockMultipartFile("image", "test.jpg", "image/jpeg", "test".getBytes()); + return new MemberSignupCommand(image, "test", "서울", 1L); + } + + public static Member getMember(WithMockMember annotation) { + return Member.builder() + .id(1L) + .name("test") + .role(annotation.authority()) + .authType(AuthType.OAUTH) + .sugarContents(0.0) + .profession(null) + .region(null) + .image(null) + .build(); + } } diff --git a/src/test/java/com/stumeet/server/template/ApiTest.java b/src/test/java/com/stumeet/server/template/ApiTest.java index 6185174d..ecbaf37b 100644 --- a/src/test/java/com/stumeet/server/template/ApiTest.java +++ b/src/test/java/com/stumeet/server/template/ApiTest.java @@ -2,23 +2,19 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.stumeet.server.helper.TestAwsS3Config; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Order; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Import; import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.DynamicPropertyRegistry; -import org.springframework.test.context.DynamicPropertySource; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.filter.CharacterEncodingFilter; -import org.testcontainers.containers.GenericContainer; -import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; @@ -29,13 +25,13 @@ @SpringBootTest @ExtendWith(RestDocumentationExtension.class) @ActiveProfiles("test") +@Import({TestAwsS3Config.class}) @Testcontainers public abstract class ApiTest { protected static final DockerImageName REDIS_CONTAINER_VERSION = DockerImageName.parse("redis:5.0.3-alpine"); protected MockMvc mockMvc; - @Autowired protected ObjectMapper objectMapper;