Skip to content

Commit

Permalink
Merge pull request #71 from Stumeet/dev
Browse files Browse the repository at this point in the history
✅ [STMT-146] 최초 로그인 시 회원가입 API 테스트 코드 작성 (#70)
  • Loading branch information
zxcv9203 authored Mar 1, 2024
2 parents eb0ad45 + 38b28e4 commit 0a44f4c
Show file tree
Hide file tree
Showing 15 changed files with 408 additions and 65 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ dependencies {
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

testImplementation 'org.springframework.cloud:spring-cloud-contract-stub-runner'
testImplementation 'org.testcontainers:localstack'


}
Expand Down
84 changes: 61 additions & 23 deletions src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand All @@ -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[]
Expand All @@ -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[]
Expand All @@ -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[]
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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)));
}
}
47 changes: 23 additions & 24 deletions src/main/java/com/stumeet/server/common/config/AwsS3Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ public ResponseEntity<ApiResponse<Void>> signup(
memberAuthUseCase.signup(loginMember.getMember(), request);

return new ResponseEntity<>(
ApiResponse.success(HttpStatus.CREATED.value(), "회원가입에 성공했습니다."),
HttpStatus.CREATED
ApiResponse.success(HttpStatus.OK.value(), "회원가입에 성공했습니다."),
HttpStatus.OK
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

public record MemberSignupCommand(

@NotNull
@NotNull(message = "이미지를 첨부해주세요")
MultipartFile image,

@Size(min = 2, max = 10, message = "닉네임을 2 ~ 10자로 입력해주세요")
Expand Down
43 changes: 43 additions & 0 deletions src/test/java/com/stumeet/server/helper/TestAwsS3Config.java
Original file line number Diff line number Diff line change
@@ -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();
}

}
15 changes: 15 additions & 0 deletions src/test/java/com/stumeet/server/helper/WithMockMember.java
Original file line number Diff line number Diff line change
@@ -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;

}
Original file line number Diff line number Diff line change
@@ -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<WithMockMember> {

@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;
}
}
Loading

0 comments on commit 0a44f4c

Please sign in to comment.