diff --git a/build.gradle b/build.gradle index cb85a763..389f343a 100644 --- a/build.gradle +++ b/build.gradle @@ -102,6 +102,8 @@ dependencies { // lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + testCompileOnly 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' // DB runtimeOnly 'com.h2database:h2' diff --git a/src/main/java/com/smeme/server/dto/auth/token/TokenResponseDTO.java b/src/main/java/com/smeme/server/dto/auth/token/TokenResponseDTO.java index 2274346f..6e23797f 100644 --- a/src/main/java/com/smeme/server/dto/auth/token/TokenResponseDTO.java +++ b/src/main/java/com/smeme/server/dto/auth/token/TokenResponseDTO.java @@ -7,4 +7,8 @@ public record TokenResponseDTO( String accessToken, String refreshToken ) { + + public static TokenResponseDTO of(String accessToken, String refreshToken) { + return new TokenResponseDTO(accessToken, refreshToken); + } } diff --git a/src/main/java/com/smeme/server/dto/goal/GoalResponseDTO.java b/src/main/java/com/smeme/server/dto/goal/GoalResponseDTO.java index 1c192cf9..b1c3005d 100644 --- a/src/main/java/com/smeme/server/dto/goal/GoalResponseDTO.java +++ b/src/main/java/com/smeme/server/dto/goal/GoalResponseDTO.java @@ -1,8 +1,18 @@ package com.smeme.server.dto.goal; +import com.smeme.server.model.goal.Goal; + public record GoalResponseDTO( String name, String way, String detail ) { + + public static GoalResponseDTO of(Goal goal) { + return new GoalResponseDTO( + goal.getType().name(), + goal.getWay(), + goal.getDetail() + ); + } } diff --git a/src/main/java/com/smeme/server/model/Correction.java b/src/main/java/com/smeme/server/model/Correction.java index 299cc418..67f5d021 100644 --- a/src/main/java/com/smeme/server/model/Correction.java +++ b/src/main/java/com/smeme/server/model/Correction.java @@ -10,6 +10,7 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -31,6 +32,7 @@ public class Correction { @JoinColumn(name = "diary_id") private Diary diary; + @Builder public Correction(String beforeSentence, String afterSentence, Diary diary) { this.beforeSentence = beforeSentence; this.afterSentence = afterSentence; diff --git a/src/main/java/com/smeme/server/model/Diary.java b/src/main/java/com/smeme/server/model/Diary.java index ab0bc62e..d0c4f036 100644 --- a/src/main/java/com/smeme/server/model/Diary.java +++ b/src/main/java/com/smeme/server/model/Diary.java @@ -6,6 +6,7 @@ import static java.util.Objects.nonNull; import jakarta.persistence.*; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -47,6 +48,7 @@ public class Diary extends BaseTimeEntity { @OneToMany(mappedBy = "diary") private final List corrections = new ArrayList<>(); + @Builder public Diary(String content, Topic topic, Member member) { this.content = content; this.targetLang = member.getTargetLang(); diff --git a/src/main/java/com/smeme/server/model/badge/Badge.java b/src/main/java/com/smeme/server/model/badge/Badge.java index 06cd8621..f894ab69 100644 --- a/src/main/java/com/smeme/server/model/badge/Badge.java +++ b/src/main/java/com/smeme/server/model/badge/Badge.java @@ -7,10 +7,13 @@ import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; +import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; @Entity @Getter +@NoArgsConstructor public class Badge { @Id @@ -23,4 +26,13 @@ public class Badge { private String name; private String imageUrl; + + + @Builder + public Badge(Long id, BadgeType type, String name, String imageUrl) { + this.id = id; + this.type = type; + this.name = name; + this.imageUrl = imageUrl; + } } diff --git a/src/main/java/com/smeme/server/model/goal/Goal.java b/src/main/java/com/smeme/server/model/goal/Goal.java index 2ed2f6a6..fd599a53 100644 --- a/src/main/java/com/smeme/server/model/goal/Goal.java +++ b/src/main/java/com/smeme/server/model/goal/Goal.java @@ -8,6 +8,7 @@ import jakarta.persistence.Enumerated; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; +import lombok.Builder; import lombok.Getter; @Entity @@ -26,4 +27,11 @@ public class Goal { private String way; private String detail; + + @Builder + public Goal(GoalType type, String way, String detail) { + this.type = type; + this.way = way; + this.detail = detail; + } } diff --git a/src/main/java/com/smeme/server/model/topic/Topic.java b/src/main/java/com/smeme/server/model/topic/Topic.java index 9e65ef4e..92cf4ee6 100644 --- a/src/main/java/com/smeme/server/model/topic/Topic.java +++ b/src/main/java/com/smeme/server/model/topic/Topic.java @@ -3,6 +3,7 @@ import static jakarta.persistence.GenerationType.*; import jakarta.persistence.*; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -20,4 +21,10 @@ public class Topic { private Category category; private String content; + + @Builder + public Topic(Category category, String content) { + this.category = category; + this.content = content; + } } diff --git a/src/main/resources/static/docs/open-api-3.0.1.json b/src/main/resources/static/docs/open-api-3.0.1.json index 054a91df..816a5756 100644 --- a/src/main/resources/static/docs/open-api-3.0.1.json +++ b/src/main/resources/static/docs/open-api-3.0.1.json @@ -10,6 +10,78 @@ } ], "tags" : [ ], "paths" : { + "/api/v2/auth" : { + "post" : { + "tags" : [ "Auth" ], + "summary" : "소셜로그인", + "description" : "소셜로그인", + "operationId" : "소셜로그인 Example", + "parameters" : [ { + "name" : "Authorization", + "in" : "header", + "description" : "소셜 로그인 토큰", + "required" : true, + "schema" : { + "type" : "string" + }, + "example" : "TEST SOCIAL_ACCESS_TOKEN" + } ], + "requestBody" : { + "content" : { + "application/json;charset=UTF-8" : { + "schema" : { + "$ref" : "#/components/schemas/api-v2-auth702405657" + }, + "examples" : { + "소셜로그인 Example" : { + "value" : "{\n \"social\" : \"KAKAO\",\n \"fcmToken\" : \"testfcmtoken\"\n}" + } + } + } + } + }, + "responses" : { + "200" : { + "description" : "200", + "content" : { + "application/json;charset=UTF-8" : { + "schema" : { + "$ref" : "#/components/schemas/api-v2-auth1857992220" + }, + "examples" : { + "소셜로그인 Example" : { + "value" : "{\n \"success\" : true,\n \"message\" : \"소셜로그인 성공\",\n \"data\" : {\n \"accessToken\" : \"testaccesstoken\",\n \"refreshToken\" : \"testrefreshtoken\",\n \"isRegistered\" : true,\n \"hasPlan\" : true\n }\n}" + } + } + } + } + } + } + }, + "delete" : { + "tags" : [ "Auth" ], + "summary" : "회원탈퇴 API", + "description" : "회원탈퇴 API", + "operationId" : "회원탈퇴 Example", + "responses" : { + "200" : { + "description" : "200", + "content" : { + "application/json;charset=UTF-8" : { + "schema" : { + "$ref" : "#/components/schemas/api-v2-corrections-correctionId594740350" + }, + "examples" : { + "회원탈퇴 Example" : { + "value" : "{\n \"success\" : true,\n \"message\" : \"회원 탈퇴 성공\",\n \"data\" : null\n}" + } + } + } + } + } + } + } + }, "/api/v2/diaries" : { "get" : { "tags" : [ "Diary" ], @@ -122,6 +194,45 @@ } } }, + "/api/v2/members" : { + "patch" : { + "tags" : [ "Member" ], + "summary" : "유저 프로필 업데이트", + "description" : "유저 프로필 업데이트", + "operationId" : "유저 프로필 업데이트 성공 Example", + "requestBody" : { + "content" : { + "application/json;charset=UTF-8" : { + "schema" : { + "$ref" : "#/components/schemas/api-v2-corrections-correctionId486549215" + }, + "examples" : { + "유저 프로필 업데이트 성공 Example" : { + "value" : "{\n \"username\" : \"홍길동\",\n \"termAccepted\" : true\n}" + } + } + } + } + }, + "responses" : { + "200" : { + "description" : "200", + "content" : { + "application/json;charset=UTF-8" : { + "schema" : { + "$ref" : "#/components/schemas/api-v2-members1042223039" + }, + "examples" : { + "유저 프로필 업데이트 성공 Example" : { + "value" : "{\n \"success\" : true,\n \"message\" : \"유저 프로필 업데이트 성공\",\n \"data\" : {\n \"username\" : null,\n \"target\" : \"자기계발\",\n \"way\" : \"주 5회 이상 오늘 하루를 돌아보는 일기 작성하기\",\n \"detail\" : \"사전 없이 일기 완성\\nsmeem 연속 일기 배지 획득\",\n \"targetLang\" : \"en\",\n \"hasPushAlarm\" : false,\n \"trainingTime\" : {\n \"day\" : \"MON\",\n \"hour\" : 10,\n \"minute\" : 30\n },\n \"badge\" : {\n \"id\" : 1,\n \"name\" : \"연속 3일 일기 뱃지\",\n \"type\" : \"COMBO\",\n \"imageUrl\" : \"https://m.s3.ap-northeast-2.amazonaws.com/badge/streak.png\"\n }\n }\n}" + } + } + } + } + } + } + } + }, "/api/v2/test" : { "get" : { "tags" : [ "api" ], @@ -134,7 +245,7 @@ "content" : { "application/json;charset=UTF-8" : { "schema" : { - "$ref" : "#/components/schemas/api-v2-diaries-diaryId594740350" + "$ref" : "#/components/schemas/api-v2-corrections-correctionId594740350" }, "examples" : { "test-docs" : { @@ -147,6 +258,150 @@ } } }, + "/api/v2/auth/sign-out" : { + "post" : { + "tags" : [ "Auth" ], + "summary" : "로그아웃 API", + "description" : "로그아웃 API", + "operationId" : "로그아웃 Example", + "responses" : { + "200" : { + "description" : "200", + "content" : { + "application/json;charset=UTF-8" : { + "schema" : { + "$ref" : "#/components/schemas/api-v2-corrections-correctionId594740350" + }, + "examples" : { + "로그아웃 Example" : { + "value" : "{\n \"success\" : true,\n \"message\" : \"로그아웃 성공\",\n \"data\" : null\n}" + } + } + } + } + } + } + } + }, + "/api/v2/auth/token" : { + "post" : { + "tags" : [ "Auth" ], + "summary" : "토큰재발급 API", + "description" : "토큰재발급 API", + "operationId" : "토큰재발급 Example", + "responses" : { + "200" : { + "description" : "200", + "content" : { + "application/json;charset=UTF-8" : { + "schema" : { + "$ref" : "#/components/schemas/api-v2-auth-token622381957" + }, + "examples" : { + "토큰재발급 Example" : { + "value" : "{\n \"success\" : true,\n \"message\" : \"토큰 발급 성공\",\n \"data\" : {\n \"accessToken\" : \"testaccesstoken\",\n \"refreshToken\" : \"testrefreshtoken\"\n }\n}" + } + } + } + } + } + } + } + }, + "/api/v2/corrections/{correctionId}" : { + "delete" : { + "tags" : [ "Correction" ], + "summary" : "첨삭 삭제", + "description" : "첨삭 삭제", + "operationId" : "첨삭 삭제 Example", + "parameters" : [ { + "name" : "correctionId", + "in" : "path", + "description" : "", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/api-v2-corrections-correctionId486549215" + }, + "examples" : { + "첨삭 삭제 Example" : { + "value" : "{\n \"sentence\" : \"correction test sentence\",\n \"content\" : \"correction test content\"\n}" + } + } + } + } + }, + "responses" : { + "200" : { + "description" : "200", + "content" : { + "application/json;charset=UTF-8" : { + "schema" : { + "$ref" : "#/components/schemas/api-v2-corrections-correctionId594740350" + }, + "examples" : { + "첨삭 삭제 Example" : { + "value" : "{\n \"success\" : true,\n \"message\" : \"첨삭 삭제 성공\",\n \"data\" : null\n}" + } + } + } + } + } + } + }, + "patch" : { + "tags" : [ "Correction" ], + "summary" : "첨삭 수정", + "description" : "첨삭 수정", + "operationId" : "첨삭 수정 Example", + "parameters" : [ { + "name" : "correctionId", + "in" : "path", + "description" : "", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "requestBody" : { + "content" : { + "application/json;charset=UTF-8" : { + "schema" : { + "$ref" : "#/components/schemas/api-v2-corrections-correctionId-539047035" + }, + "examples" : { + "첨삭 수정 Example" : { + "value" : "{\n \"sentence\" : \"correction test sentence\",\n \"content\" : \"correction test content\"\n}" + } + } + } + } + }, + "responses" : { + "200" : { + "description" : "200", + "content" : { + "application/json;charset=UTF-8" : { + "schema" : { + "$ref" : "#/components/schemas/api-v2-corrections-correctionId594740350" + }, + "examples" : { + "첨삭 수정 Example" : { + "value" : "{\n \"success\" : true,\n \"message\" : \"첨삭 수정 성공\",\n \"data\" : null\n}" + } + } + } + } + } + } + } + }, "/api/v2/diaries/{diaryId}" : { "get" : { "tags" : [ "Diary" ], @@ -200,7 +455,7 @@ "content" : { "application/json;charset=UTF-8" : { "schema" : { - "$ref" : "#/components/schemas/api-v2-diaries-diaryId594740350" + "$ref" : "#/components/schemas/api-v2-corrections-correctionId594740350" }, "examples" : { "일기 삭제 성공 Example" : { @@ -246,7 +501,7 @@ "content" : { "application/json;charset=UTF-8" : { "schema" : { - "$ref" : "#/components/schemas/api-v2-diaries-diaryId594740350" + "$ref" : "#/components/schemas/api-v2-corrections-correctionId594740350" }, "examples" : { "일기 수정 성공 Example" : { @@ -293,23 +548,23 @@ } } }, - "/api/v2/topics/random" : { + "/api/v2/members/badges" : { "get" : { - "tags" : [ "Topic" ], - "summary" : "랜덤 주제 조회", - "description" : "랜덤 주제 조회", - "operationId" : "랜덤 주제 조회 성공 Example", + "tags" : [ "Badge" ], + "summary" : "뱃지 목록 조회", + "description" : "뱃지 목록 조회", + "operationId" : "뱃지 목록 조회 Example", "responses" : { "200" : { "description" : "200", "content" : { "application/json;charset=UTF-8" : { "schema" : { - "$ref" : "#/components/schemas/api-v2-topics-random459952356" + "$ref" : "#/components/schemas/api-v2-members-badges-599940611" }, "examples" : { - "랜덤 주제 조회 성공 Example" : { - "value" : "{\n \"success\" : true,\n \"message\" : \"랜덤 주제 조회 성공\",\n \"data\" : {\n \"topicId\" : 1,\n \"content\" : \"가보고 싶은 해외 여행 지가 있다면 소개해 주세요!\"\n }\n}" + "뱃지 목록 조회 Example" : { + "value" : "{\n \"success\" : true,\n \"message\" : \"뱃지 리스트 조회 성공\",\n \"data\" : {\n \"badges\" : [ {\n \"id\" : 1,\n \"name\" : \"연속 3일 일기 뱃지\",\n \"type\" : \"COMBO\",\n \"imageUrl\" : \"https://m.s3.ap-northeast-2.amazonaws.com/badge/streak.png\"\n }, {\n \"id\" : 2,\n \"name\" : \"연속 3일 일기 뱃지\",\n \"type\" : \"COMBO\",\n \"imageUrl\" : \"https://m.s3.ap-northeast-2.amazonaws.com/badge/streak.png\"\n } ]\n }\n}" } } } @@ -317,40 +572,540 @@ } } } - } - }, - "components" : { - "schemas" : { - "api-v2-diaries-1124915790" : { - "type" : "object", - "properties" : { - "topicId" : { - "type" : "number", - "description" : "랜덤 주제 id(8~54) or null" - }, - "content" : { - "type" : "string", - "description" : "일기 내용" + }, + "/api/v2/members/me" : { + "get" : { + "tags" : [ "Member" ], + "summary" : "사용자 정보 조회", + "description" : "사용자 정보 조회", + "operationId" : "사용자 정보 조회 성공 Example", + "responses" : { + "200" : { + "description" : "200", + "content" : { + "application/json;charset=UTF-8" : { + "schema" : { + "$ref" : "#/components/schemas/api-v2-members1042223039" + }, + "examples" : { + "사용자 정보 조회 성공 Example" : { + "value" : "{\n \"success\" : true,\n \"message\" : \"사용자 정보 조회 성공\",\n \"data\" : {\n \"username\" : null,\n \"target\" : \"자기계발\",\n \"way\" : \"주 5회 이상 오늘 하루를 돌아보는 일기 작성하기\",\n \"detail\" : \"사전 없이 일기 완성\\nsmeem 연속 일기 배지 획득\",\n \"targetLang\" : \"en\",\n \"hasPushAlarm\" : false,\n \"trainingTime\" : {\n \"day\" : \"MON\",\n \"hour\" : 10,\n \"minute\" : 30\n },\n \"badge\" : {\n \"id\" : 1,\n \"name\" : \"연속 3일 일기 뱃지\",\n \"type\" : \"COMBO\",\n \"imageUrl\" : \"https://m.s3.ap-northeast-2.amazonaws.com/badge/streak.png\"\n }\n }\n}" + } + } + } + } } } - }, - "api-v2-diaries-diaryId1231150871" : { - "type" : "object", - "properties" : { - "topicId" : { - "type" : "number", - "description" : "랜덤 주제 id" - }, + } + }, + "/api/v2/members/plan" : { + "patch" : { + "tags" : [ "Member" ], + "summary" : "사용자 수정 API", + "description" : "사용자 수정 API", + "operationId" : "사용자 정보 수정 성공 Example", + "requestBody" : { "content" : { - "type" : "string", - "description" : "수정할 일기 내용" + "application/json;charset=UTF-8" : { + "schema" : { + "$ref" : "#/components/schemas/api-v2-corrections-correctionId486549215" + }, + "examples" : { + "사용자 정보 수정 성공 Example" : { + "value" : "{\n \"target\" : \"DEVELOP\",\n \"trainingTime\" : {\n \"day\" : \"MON\",\n \"hour\" : 10,\n \"minute\" : 30\n },\n \"hasAlarm\" : true\n}" + } + } + } + } + }, + "responses" : { + "200" : { + "description" : "200", + "content" : { + "application/json;charset=UTF-8" : { + "schema" : { + "$ref" : "#/components/schemas/api-v2-corrections-correctionId594740350" + }, + "examples" : { + "사용자 정보 수정 성공 Example" : { + "value" : "{\n \"success\" : true,\n \"message\" : \"회원 학습 계획 업데이트 성공\",\n \"data\" : null\n}" + } + } + } + } } } - }, - "api-v2-diaries-486735900" : { - "type" : "object", - "properties" : { - "data" : { + } + }, + "/api/v2/members/push" : { + "patch" : { + "tags" : [ "Member" ], + "summary" : "푸시 알림 동의여부 수정", + "description" : "푸시 알림 동의여부 수정", + "operationId" : "푸시 알림 동의여부 성공 Example", + "requestBody" : { + "content" : { + "application/json;charset=UTF-8" : { + "schema" : { + "$ref" : "#/components/schemas/api-v2-members-push-1005876473" + }, + "examples" : { + "푸시 알림 동의여부 성공 Example" : { + "value" : "{\n \"hasAlarm\" : true\n}" + } + } + } + } + }, + "responses" : { + "200" : { + "description" : "200", + "content" : { + "application/json;charset=UTF-8" : { + "schema" : { + "$ref" : "#/components/schemas/api-v2-corrections-correctionId594740350" + }, + "examples" : { + "푸시 알림 동의여부 성공 Example" : { + "value" : "{\n \"success\" : true,\n \"message\" : \"회원 푸시알람 동의여부 업데이트 성공\",\n \"data\" : null\n}" + } + } + } + } + } + } + } + }, + "/api/v2/topics/random" : { + "get" : { + "tags" : [ "Topic" ], + "summary" : "랜덤 주제 조회", + "description" : "랜덤 주제 조회", + "operationId" : "랜덤 주제 조회 성공 Example", + "responses" : { + "200" : { + "description" : "200", + "content" : { + "application/json;charset=UTF-8" : { + "schema" : { + "$ref" : "#/components/schemas/api-v2-topics-random459952356" + }, + "examples" : { + "랜덤 주제 조회 성공 Example" : { + "value" : "{\n \"success\" : true,\n \"message\" : \"랜덤 주제 조회 성공\",\n \"data\" : {\n \"topicId\" : 1,\n \"content\" : \"가보고 싶은 해외 여행 지가 있다면 소개해 주세요!\"\n }\n}" + } + } + } + } + } + } + } + }, + "/api/v2/corrections/diary/{diaryId}" : { + "post" : { + "tags" : [ "Correction" ], + "summary" : "첨삭 추가", + "description" : "첨삭 추가", + "operationId" : "첨삭 추가 Example", + "parameters" : [ { + "name" : "diaryId", + "in" : "path", + "description" : "", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "requestBody" : { + "content" : { + "application/json;charset=UTF-8" : { + "schema" : { + "$ref" : "#/components/schemas/api-v2-corrections-correctionId-539047035" + }, + "examples" : { + "첨삭 추가 Example" : { + "value" : "{\n \"sentence\" : \"correction test sentence\",\n \"content\" : \"correction test content\"\n}" + } + } + } + } + }, + "responses" : { + "201" : { + "description" : "201", + "headers" : { + "Location" : { + "description" : "생성된 첨삭 조회 URI", + "schema" : { + "type" : "string" + } + } + }, + "content" : { + "application/json;charset=UTF-8" : { + "schema" : { + "$ref" : "#/components/schemas/api-v2-corrections-diary-diaryId1208203954" + }, + "examples" : { + "첨삭 추가 Example" : { + "value" : "{\n \"success\" : true,\n \"message\" : \"일기 첨삭 성공\",\n \"data\" : {\n \"diaryId\" : 1,\n \"badges\" : [ {\n \"name\" : \"연속 3일 일기 뱃지\",\n \"imageUrl\" : \"https://m.s3.ap-northeast-2.amazonaws.com/badge/streak.png\"\n } ]\n }\n}" + } + } + } + } + } + } + } + }, + "/api/v2/members/nickname/check" : { + "get" : { + "tags" : [ "Member" ], + "summary" : "닉네임 중복 체크", + "description" : "닉네임 중복 체크", + "operationId" : "닉네임 중복 체크 성공 Example", + "responses" : { + "200" : { + "description" : "200", + "content" : { + "application/json;charset=UTF-8" : { + "schema" : { + "$ref" : "#/components/schemas/api-v2-corrections-correctionId594740350" + }, + "examples" : { + "닉네임 중복 체크 성공 Example" : { + "value" : "{\n \"success\" : true,\n \"message\" : \"닉네임 중복 체크 성공\",\n \"data\" : null\n}" + } + } + } + } + } + } + } + } + }, + "components" : { + "schemas" : { + "api-v2-members-badges-599940611" : { + "type" : "object", + "properties" : { + "data" : { + "type" : "object", + "properties" : { + "badges" : { + "type" : "array", + "description" : "뱃지 목록", + "items" : { + "type" : "object", + "properties" : { + "imageUrl" : { + "type" : "string", + "description" : "뱃지 이미지 URL" + }, + "name" : { + "type" : "string", + "description" : "뱃지 이름" + }, + "id" : { + "type" : "number", + "description" : "뱃지 ID" + }, + "type" : { + "type" : "string", + "description" : "뱃지 타입" + } + } + } + } + }, + "description" : "응답 데이터" + }, + "success" : { + "type" : "boolean", + "description" : "응답 성공 여부" + }, + "message" : { + "type" : "string", + "description" : "응답 메시지" + } + } + }, + "api-v2-members1042223039" : { + "type" : "object", + "properties" : { + "data" : { + "type" : "object", + "properties" : { + "badge" : { + "type" : "object", + "properties" : { + "imageUrl" : { + "type" : "string", + "description" : "뱃지 이미지" + }, + "name" : { + "type" : "string", + "description" : "뱃지 이름" + }, + "id" : { + "type" : "number", + "description" : "뱃지 아이디" + }, + "type" : { + "type" : "string", + "description" : "뱃지 설명" + } + }, + "description" : "뱃지" + }, + "targetLang" : { + "type" : "string", + "description" : "목표 언어" + }, + "trainingTime" : { + "type" : "object", + "properties" : { + "hour" : { + "type" : "number", + "description" : "학습 시간" + }, + "day" : { + "type" : "string", + "description" : "학습 요일" + }, + "minute" : { + "type" : "number", + "description" : "학습 분" + } + }, + "description" : "학습 시간" + }, + "hasPushAlarm" : { + "type" : "boolean", + "description" : "푸시 알림 여부" + }, + "detail" : { + "type" : "string", + "description" : "목표 달성 방법" + }, + "way" : { + "type" : "string", + "description" : "목표 달성 방법" + }, + "target" : { + "type" : "string", + "description" : "학습목표" + } + }, + "description" : "응답 데이터" + }, + "success" : { + "type" : "boolean", + "description" : "응답 성공 여부" + }, + "message" : { + "type" : "string", + "description" : "응답 메시지" + } + } + }, + "api-v2-diaries-1124915790" : { + "type" : "object", + "properties" : { + "topicId" : { + "type" : "number", + "description" : "랜덤 주제 id(8~54) or null" + }, + "content" : { + "type" : "string", + "description" : "일기 내용" + } + } + }, + "api-v2-auth1857992220" : { + "type" : "object", + "properties" : { + "data" : { + "type" : "object", + "properties" : { + "isRegistered" : { + "type" : "boolean", + "description" : "서비스 등록 여부" + }, + "hasPlan" : { + "type" : "boolean", + "description" : "학습 계획 설정 여부" + }, + "accessToken" : { + "type" : "string", + "description" : "서비스 액세스 토큰" + }, + "refreshToken" : { + "type" : "string", + "description" : "서비스 리프레시 토큰" + } + }, + "description" : "응답 데이터" + }, + "success" : { + "type" : "boolean", + "description" : "응답 성공 여부" + }, + "message" : { + "type" : "string", + "description" : "응답 메시지" + } + } + }, + "api-v2-auth702405657" : { + "type" : "object", + "properties" : { + "social" : { + "type" : "string", + "description" : "소셜로그인 타입" + }, + "fcmToken" : { + "type" : "string", + "description" : "FCM Token" + } + } + }, + "api-v2-goals-type1705313935" : { + "type" : "object", + "properties" : { + "data" : { + "type" : "object", + "properties" : { + "name" : { + "type" : "string", + "description" : "학습 목표 이름" + }, + "detail" : { + "type" : "string", + "description" : "학습 목표 내용" + }, + "way" : { + "type" : "string", + "description" : "학습 목표 방식" + } + }, + "description" : "응답 데이터" + }, + "success" : { + "type" : "boolean", + "description" : "응답 성공 여부" + }, + "message" : { + "type" : "string", + "description" : "응답 메시지" + } + } + }, + "api-v2-members-push-1005876473" : { + "type" : "object", + "properties" : { + "hasAlarm" : { + "type" : "boolean", + "description" : "푸시 알림 동의여부" + } + } + }, + "api-v2-corrections-correctionId594740350" : { + "type" : "object", + "properties" : { + "success" : { + "type" : "boolean", + "description" : "응답 성공 여부" + }, + "message" : { + "type" : "string", + "description" : "응답 메시지" + } + } + }, + "api-v2-topics-random459952356" : { + "type" : "object", + "properties" : { + "data" : { + "type" : "object", + "properties" : { + "topicId" : { + "type" : "number", + "description" : "랜덤 주제 id" + }, + "content" : { + "type" : "string", + "description" : "랜덤 주제 내용" + } + }, + "description" : "응답 데이터" + }, + "success" : { + "type" : "boolean", + "description" : "응답 성공 여부" + }, + "message" : { + "type" : "string", + "description" : "응답 메시지" + } + } + }, + "api-v2-corrections-diary-diaryId1208203954" : { + "type" : "object", + "properties" : { + "data" : { + "type" : "object", + "properties" : { + "badges" : { + "type" : "array", + "description" : "뱃지 목록", + "items" : { + "type" : "object", + "properties" : { + "imageUrl" : { + "type" : "string", + "description" : "뱃지 이미지 URL" + }, + "name" : { + "type" : "string", + "description" : "뱃지 이름" + } + } + } + }, + "diaryId" : { + "type" : "number", + "description" : "일기 ID" + } + }, + "description" : "응답 데이터" + }, + "success" : { + "type" : "boolean", + "description" : "응답 성공 여부" + }, + "message" : { + "type" : "string", + "description" : "응답 메시지" + } + } + }, + "api-v2-diaries-diaryId1231150871" : { + "type" : "object", + "properties" : { + "topicId" : { + "type" : "number", + "description" : "랜덤 주제 id" + }, + "content" : { + "type" : "string", + "description" : "수정할 일기 내용" + } + } + }, + "api-v2-diaries-486735900" : { + "type" : "object", + "properties" : { + "data" : { "type" : "object", "properties" : { "badges" : { @@ -491,34 +1246,16 @@ } } }, - "api-v2-goals-type1705313935" : { + "api-v2-corrections-correctionId-539047035" : { "type" : "object", "properties" : { - "data" : { - "type" : "object", - "properties" : { - "name" : { - "type" : "string", - "description" : "학습 목표 이름" - }, - "detail" : { - "type" : "string", - "description" : "학습 목표 내용" - }, - "way" : { - "type" : "string", - "description" : "학습 목표 방식" - } - }, - "description" : "응답 데이터" - }, - "success" : { - "type" : "boolean", - "description" : "응답 성공 여부" + "sentence" : { + "type" : "string", + "description" : "첨삭 전 문장" }, - "message" : { + "content" : { "type" : "string", - "description" : "응답 메시지" + "description" : "첨삭 후 문장" } } }, @@ -558,32 +1295,19 @@ } } }, - "api-v2-diaries-diaryId594740350" : { - "type" : "object", - "properties" : { - "success" : { - "type" : "boolean", - "description" : "응답 성공 여부" - }, - "message" : { - "type" : "string", - "description" : "응답 메시지" - } - } - }, - "api-v2-topics-random459952356" : { + "api-v2-auth-token622381957" : { "type" : "object", "properties" : { "data" : { "type" : "object", "properties" : { - "topicId" : { - "type" : "number", - "description" : "랜덤 주제 id" + "accessToken" : { + "type" : "string", + "description" : "서비스 액세스 토큰" }, - "content" : { + "refreshToken" : { "type" : "string", - "description" : "랜덤 주제 내용" + "description" : "서비스 리프레시 토큰" } }, "description" : "응답 데이터" @@ -597,6 +1321,9 @@ "description" : "응답 메시지" } } + }, + "api-v2-corrections-correctionId486549215" : { + "type" : "object" } } } diff --git a/src/test/java/com/smeme/server/controller/AuthControllerTest.java b/src/test/java/com/smeme/server/controller/AuthControllerTest.java new file mode 100644 index 00000000..1345a45b --- /dev/null +++ b/src/test/java/com/smeme/server/controller/AuthControllerTest.java @@ -0,0 +1,179 @@ +package com.smeme.server.controller; + + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.smeme.server.fixture.auth.AuthFixture; +import com.smeme.server.fixture.correction.CorrectionFixture; +import lombok.val; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.ResponseEntity; + +import java.net.URI; +import java.security.Principal; + +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static com.smeme.server.util.ApiResponse.success; +import static com.smeme.server.util.message.ResponseMessage.*; +import static org.mockito.Mockito.when; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.JsonFieldType.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DisplayName("AuthController 테스트") +@WebMvcTest(AuthController.class) +public class AuthControllerTest extends BaseControllerTest { + + @MockBean + AuthController authController; + + @MockBean + Principal principal; + + private final String DEFAULT_URL = "/api/v2/auth"; + private final String TAG = "Auth"; + + private static final String SOCIAL_ACCESS_TOKEN = "TEST SOCIAL_ACCESS_TOKEN"; + + @DisplayName("소셜로그인 API 성공") + @Test + void success_signIn() throws Exception { + + val resources = ResourceSnippetParameters.builder() + .tag(TAG) + .description("소셜로그인") + .requestHeaders( + headerWithName("Authorization").description("소셜 로그인 토큰") + ) + .requestFields( + fieldWithPath("social").type(STRING).description("소셜로그인 타입"), + fieldWithPath("fcmToken").type(STRING).description("FCM Token") + ) + .responseFields( + fieldWithPath("success").type(BOOLEAN).description("응답 성공 여부"), + fieldWithPath("message").type(STRING).description("응답 메시지"), + fieldWithPath("data").type(OBJECT).description("응답 데이터"), + fieldWithPath("data.accessToken").type(STRING).description("서비스 액세스 토큰"), + fieldWithPath("data.refreshToken").type(STRING).description("서비스 리프레시 토큰"), + fieldWithPath("data.isRegistered").type(BOOLEAN).description("서비스 등록 여부"), + fieldWithPath("data.hasPlan").type(BOOLEAN).description("학습 계획 설정 여부") + ) + .build(); + // when + val result = ResponseEntity.ok(success(SUCCESS_SIGNIN.getMessage(), AuthFixture.createSignInResponseDTO())); + + when(authController.signIn(SOCIAL_ACCESS_TOKEN , AuthFixture.createSignInRequestDTO())) + .thenReturn(result); + + + mockMvc.perform(post(DEFAULT_URL) + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON) + .principal(principal) + .header("Authorization", SOCIAL_ACCESS_TOKEN) + .content(objectMapper.writeValueAsString(AuthFixture.createSignInRequestDTO()))) + .andDo( + document("소셜로그인 Example", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource(resources) + )) + .andExpect(status().isOk()); + } + + @DisplayName("토큰재발급 API 성공") + @Test + void success_reissueToken() throws Exception { + val resources = ResourceSnippetParameters.builder() + .tag(TAG) + .description("토큰재발급 API") + .responseFields( + fieldWithPath("success").type(BOOLEAN).description("응답 성공 여부"), + fieldWithPath("message").type(STRING).description("응답 메시지"), + fieldWithPath("data").type(OBJECT).description("응답 데이터"), + fieldWithPath("data.accessToken").type(STRING).description("서비스 액세스 토큰"), + fieldWithPath("data.refreshToken").type(STRING).description("서비스 리프레시 토큰") + ) + .build(); + // when + val result = ResponseEntity.ok(success(SUCCESS_ISSUE_TOKEN.getMessage(), AuthFixture.createTokenResponseDTO())); + + when(authController.reissueToken(principal)).thenReturn(result); + + mockMvc.perform(post(DEFAULT_URL + "/token") + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON) + .principal(principal)) + .andDo( + document("토큰재발급 Example", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource(resources) + )) + .andExpect(status().isOk()); + } + + @DisplayName("로그아웃 API 성공") + @Test + void success_signOut() throws Exception { + val resources = ResourceSnippetParameters.builder() + .tag(TAG) + .description("로그아웃 API") + .responseFields( + fieldWithPath("success").type(BOOLEAN).description("응답 성공 여부"), + fieldWithPath("message").type(STRING).description("응답 메시지"), + fieldWithPath("data").type(NULL).description("응답 데이터") + ) + .build(); + // when + val result = ResponseEntity.ok(success(SUCCESS_SIGNOUT.getMessage())); + + when(authController.signOut(principal)).thenReturn(result); + + mockMvc.perform(post(DEFAULT_URL + "/sign-out") + .principal(principal)) + .andDo( + document("로그아웃 Example", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource(resources) + )) + .andExpect(status().isOk()); + } + + @DisplayName("회원탈퇴 API 성공") + @Test + void success_withDrawl() throws Exception{ + val resources = ResourceSnippetParameters.builder() + .tag(TAG) + .description("회원탈퇴 API") + .responseFields( + fieldWithPath("success").type(BOOLEAN).description("응답 성공 여부"), + fieldWithPath("message").type(STRING).description("응답 메시지"), + fieldWithPath("data").type(NULL).description("응답 데이터") + ) + .build(); + // when + val result = ResponseEntity.ok(success(SUCCESS_WITHDRAW.getMessage())); + + when(authController.withDrawl(principal)).thenReturn(result); + + mockMvc.perform(delete(DEFAULT_URL) + .principal(principal)) + .andDo( + document("회원탈퇴 Example", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource(resources) + )) + .andExpect(status().isOk()); + } +} diff --git a/src/test/java/com/smeme/server/controller/BadgeControllerTest.java b/src/test/java/com/smeme/server/controller/BadgeControllerTest.java new file mode 100644 index 00000000..a22c57b3 --- /dev/null +++ b/src/test/java/com/smeme/server/controller/BadgeControllerTest.java @@ -0,0 +1,75 @@ +package com.smeme.server.controller; + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.smeme.server.fixture.badge.BadgeFixture; +import lombok.val; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.ResponseEntity; + +import java.security.Principal; + +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static com.smeme.server.util.ApiResponse.success; +import static com.smeme.server.util.message.ResponseMessage.SUCCESS_GET_BADGES; +import static org.mockito.Mockito.when; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.payload.JsonFieldType.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DisplayName("BadgeController 테스트") +@WebMvcTest(BadgeController.class) +public class BadgeControllerTest extends BaseControllerTest { + + @MockBean + BadgeController badgeController; + @MockBean + Principal principal; + + private final String DEFAULT_URL = "/api/v2/members/badges"; + private final String TAG = "Badge"; + + @DisplayName("뱃지 목록 조회 API 성공") + @Test + void success_getBadgesList() throws Exception { + val resources = ResourceSnippetParameters.builder() + .tag(TAG) + .description("뱃지 목록 조회") + .responseFields( + fieldWithPath("success").type(BOOLEAN).description("응답 성공 여부"), + fieldWithPath("message").type(STRING).description("응답 메시지"), + fieldWithPath("data").type(OBJECT).description("응답 데이터"), + fieldWithPath("data.badges").type(ARRAY).description("뱃지 목록"), + fieldWithPath("data.badges[].id").type(NUMBER).description("뱃지 ID"), + fieldWithPath("data.badges[].name").type(STRING).description("뱃지 이름"), + fieldWithPath("data.badges[].type").type(STRING).description("뱃지 타입"), + fieldWithPath("data.badges[].imageUrl").type(STRING).description("뱃지 이미지 URL") + ) + .build(); + // when + val result = ResponseEntity.ok(success(SUCCESS_GET_BADGES.getMessage(), BadgeFixture.createBadgeListResponseDTO())); + + when(badgeController.getBadgeList(principal)) + .thenReturn(result); + + + mockMvc.perform(get(DEFAULT_URL) + .contentType(APPLICATION_JSON) + .principal(principal)) + .andDo( + document("뱃지 목록 조회 Example", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource(resources) + )) + .andExpect(status().isOk()); + } + +} diff --git a/src/test/java/com/smeme/server/controller/CorrectionControllerTest.java b/src/test/java/com/smeme/server/controller/CorrectionControllerTest.java new file mode 100644 index 00000000..b1c6ad33 --- /dev/null +++ b/src/test/java/com/smeme/server/controller/CorrectionControllerTest.java @@ -0,0 +1,160 @@ +package com.smeme.server.controller; + + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.smeme.server.fixture.correction.CorrectionFixture; +import lombok.val; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.ResponseEntity; + +import java.net.URI; +import java.security.Principal; + +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static com.smeme.server.util.ApiResponse.success; +import static com.smeme.server.util.message.ResponseMessage.*; +import static org.mockito.Mockito.when; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.payload.JsonFieldType.*; +import static org.springframework.restdocs.payload.JsonFieldType.STRING; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DisplayName("CorrectionController 테스트") +@WebMvcTest(CorrectionController.class) +public class CorrectionControllerTest extends BaseControllerTest { + + @MockBean + CorrectionController correctionController; + + @MockBean + Principal principal; + + private final String DEFAULT_URL = "/api/v2/corrections"; + private final String TAG = "Correction"; + + private static final Long DIARY_ID = 1L; + private static final Long CORRECTION_ID = 1L; + + + + @DisplayName("첨삭 추가 API 성공") + @Test + void success_save() throws Exception { + val resources = ResourceSnippetParameters.builder() + .tag(TAG) + .description("첨삭 추가") + .requestFields( + fieldWithPath("sentence").type(STRING).description("첨삭 전 문장"), + fieldWithPath("content").type(STRING).description("첨삭 후 문장") + ) + .responseHeaders(headerWithName("Location").description("생성된 첨삭 조회 URI")) + .responseFields( + fieldWithPath("success").type(BOOLEAN).description("응답 성공 여부"), + fieldWithPath("message").type(STRING).description("응답 메시지"), + fieldWithPath("data").type(OBJECT).description("응답 데이터"), + fieldWithPath("data.diaryId").type(NUMBER).description("일기 ID"), + fieldWithPath("data.badges").type(ARRAY).description("뱃지 목록"), + fieldWithPath("data.badges[].name").type(STRING).description("뱃지 이름"), + fieldWithPath("data.badges[].imageUrl").type(STRING).description("뱃지 이미지 URL") + ) + .build(); + // when + val result = ResponseEntity. + created(URI.create(DEFAULT_URL +"/diary/" + DIARY_ID)) + .body(success(SUCCESS_CREATE_CORRECTION.getMessage(), CorrectionFixture.createCorrectionResponseDTO())); + + when(correctionController.save(principal, DIARY_ID, CorrectionFixture.createCorrectionRequestDTO())) + .thenReturn(result); + + + mockMvc.perform(post(DEFAULT_URL + "/diary/{diaryId}", DIARY_ID) + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON) + .principal(principal) + .content(objectMapper.writeValueAsString(CorrectionFixture.createCorrectionRequestDTO()))) + .andDo( + document("첨삭 추가 Example", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource(resources) + )) + .andExpect(status().isCreated()); + } + + @DisplayName("첨삭 삭제 API 성공") + @Test + void success_delete() throws Exception { + val resources = ResourceSnippetParameters.builder() + .tag(TAG) + .description("첨삭 삭제") + .responseFields( + fieldWithPath("success").type(BOOLEAN).description("응답 성공 여부"), + fieldWithPath("message").type(STRING).description("응답 메시지"), + fieldWithPath("data").type(NULL).description("응답 데이터") + ) + .build(); + // when + val result = ResponseEntity.ok(success(SUCCESS_DELETE_CORRECTION.getMessage())); + + when(correctionController.delete(CORRECTION_ID)) + .thenReturn(result); + + + mockMvc.perform(delete(DEFAULT_URL + "/{correctionId}", CORRECTION_ID) + .principal(principal) + .content(objectMapper.writeValueAsString(CorrectionFixture.createCorrectionRequestDTO()))) + .andDo( + document("첨삭 삭제 Example", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource(resources) + )) + .andExpect(status().isOk()); + } + + @DisplayName("첨삭 수정 API 성공") + @Test + void success_update() throws Exception { + val resources = ResourceSnippetParameters.builder() + .tag(TAG) + .description("첨삭 수정") + .requestFields( + fieldWithPath("sentence").type(STRING).description("첨삭 전 문장"), + fieldWithPath("content").type(STRING).description("첨삭 후 문장") + ) + .responseFields( + fieldWithPath("success").type(BOOLEAN).description("응답 성공 여부"), + fieldWithPath("message").type(STRING).description("응답 메시지"), + fieldWithPath("data").type(NULL).description("응답 데이터") + ) + .build(); + // when + val result = ResponseEntity.ok(success(SUCCESS_UPDATE_CORRECTION.getMessage())); + when(correctionController.update(CORRECTION_ID, CorrectionFixture.createCorrectionRequestDTO())) + .thenReturn(result); + + + mockMvc.perform(patch(DEFAULT_URL + "/{correctionId}", CORRECTION_ID) + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON) + .principal(principal) + .content(objectMapper.writeValueAsString(CorrectionFixture.createCorrectionRequestDTO()))) + .andDo( + document("첨삭 수정 Example", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource(resources) + )) + .andExpect(status().isOk()); + } + +} diff --git a/src/test/java/com/smeme/server/controller/MemberControllerTest.java b/src/test/java/com/smeme/server/controller/MemberControllerTest.java new file mode 100644 index 00000000..cf06ceb4 --- /dev/null +++ b/src/test/java/com/smeme/server/controller/MemberControllerTest.java @@ -0,0 +1,224 @@ +package com.smeme.server.controller; + + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.smeme.server.fixture.member.MemberFixture; +import lombok.val; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.ResponseEntity; + +import java.security.Principal; + +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static com.smeme.server.util.message.ResponseMessage.SUCCESS_UPDATE_USER_PLAN; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static com.smeme.server.util.ApiResponse.success; +import static com.smeme.server.util.message.ResponseMessage.SUCCESS_UPDATE_USER_PUSH; +import static org.mockito.Mockito.when; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; +import static org.springframework.restdocs.payload.JsonFieldType.*; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@DisplayName("Member Controller Test") +@WebMvcTest(MemberController.class) +public class MemberControllerTest extends BaseControllerTest { + + private static final String DEFAULT_URL = "/api/v2/members"; + private static final String TAG = "Member"; + + @MockBean + MemberController memberController; + + @MockBean + Principal principal; + + @Test + @DisplayName("유저 프로필 업데이트 성공") + void success_updateProfile() throws Exception { + val resources = ResourceSnippetParameters.builder() + .tag(TAG) + .description("유저 프로필 업데이트") + .responseFields( + fieldWithPath("success").type(BOOLEAN).description("응답 성공 여부"), + fieldWithPath("message").type(STRING).description("응답 메시지"), + fieldWithPath("data").type(OBJECT).description("응답 데이터"), + fieldWithPath("data.username").type(NULL).description("유저 닉네임"), + fieldWithPath("data.target").type(STRING).description("학습목표"), + fieldWithPath("data.way").type(STRING).description("목표 달성 방법"), + fieldWithPath("data.detail").type(STRING).description("목표 달성 방법"), + fieldWithPath("data.targetLang").type(STRING).description("목표 언어"), + fieldWithPath("data.hasPushAlarm").type(BOOLEAN).description("푸시 알림 여부"), + fieldWithPath("data.trainingTime").type(OBJECT).description("학습 시간"), + fieldWithPath("data.trainingTime.day").type(STRING).description("학습 요일"), + fieldWithPath("data.trainingTime.hour").type(NUMBER).description("학습 시간"), + fieldWithPath("data.trainingTime.minute").type(NUMBER).description("학습 분"), + fieldWithPath("data.badge").type(OBJECT).description("뱃지"), + fieldWithPath("data.badge.id").type(NUMBER).description("뱃지 아이디"), + fieldWithPath("data.badge.name").type(STRING).description("뱃지 이름"), + fieldWithPath("data.badge.type").type(STRING).description("뱃지 설명"), + fieldWithPath("data.badge.imageUrl").type(STRING).description("뱃지 이미지") + ) + .build(); + + val response = ResponseEntity.ok(success("유저 프로필 업데이트 성공", MemberFixture.createMemberGetResponseDTO())); + + when(memberController.updateProfile(principal, MemberFixture.createMemberUpdateRequestDTO())) + .thenReturn(response); + + mockMvc.perform(patch(DEFAULT_URL) + .contentType(APPLICATION_JSON) + .accept(APPLICATION_JSON) + .principal(principal) + .content(objectMapper.writeValueAsString(MemberFixture.createMemberUpdateRequestDTO())) + ).andDo( + document("유저 프로필 업데이트 성공 Example", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource(resources) + )) + .andExpect(status().isOk()); + } + + @DisplayName("사용자 정보 조회") + @Test + void success_getProfile() throws Exception { + val resources = ResourceSnippetParameters.builder() + .tag(TAG) + .description("사용자 정보 조회") + .responseFields( + fieldWithPath("success").type(BOOLEAN).description("응답 성공 여부"), + fieldWithPath("message").type(STRING).description("응답 메시지"), + fieldWithPath("data").type(OBJECT).description("응답 데이터"), + fieldWithPath("data.username").type(NULL).description("유저 닉네임"), + fieldWithPath("data.target").type(STRING).description("학습목표"), + fieldWithPath("data.way").type(STRING).description("목표 달성 방법"), + fieldWithPath("data.detail").type(STRING).description("목표 달성 방법"), + fieldWithPath("data.targetLang").type(STRING).description("목표 언어"), + fieldWithPath("data.hasPushAlarm").type(BOOLEAN).description("푸시 알림 여부"), + fieldWithPath("data.trainingTime").type(OBJECT).description("학습 시간"), + fieldWithPath("data.trainingTime.day").type(STRING).description("학습 요일"), + fieldWithPath("data.trainingTime.hour").type(NUMBER).description("학습 시간"), + fieldWithPath("data.trainingTime.minute").type(NUMBER).description("학습 분"), + fieldWithPath("data.badge").type(OBJECT).description("뱃지"), + fieldWithPath("data.badge.id").type(NUMBER).description("뱃지 아이디"), + fieldWithPath("data.badge.name").type(STRING).description("뱃지 이름"), + fieldWithPath("data.badge.type").type(STRING).description("뱃지 설명"), + fieldWithPath("data.badge.imageUrl").type(STRING).description("뱃지 이미지") + ) + .build(); + val result = ResponseEntity.ok(success("사용자 정보 조회 성공", MemberFixture.createMemberGetResponseDTO())); + + when(memberController.getProfile(principal)) + .thenReturn(result); + + mockMvc.perform(get(DEFAULT_URL + "/me") + .contentType(APPLICATION_JSON) + .principal(principal)) + .andDo( + document("사용자 정보 조회 성공 Example", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource(resources) + )) + .andExpect(status().isOk()); + } + + @DisplayName("Member 학습계획 수정 API 성공 ") + @Test + void success_updateUserPlan() throws Exception { + val resources = ResourceSnippetParameters.builder() + .tag(TAG) + .description("사용자 수정 API") + .responseFields( + fieldWithPath("success").type(BOOLEAN).description("응답 성공 여부"), + fieldWithPath("message").type(STRING).description("응답 메시지"), + fieldWithPath("data").type(NULL).description("응답 데이터") + ) + .build(); + val result = ResponseEntity.ok(success(SUCCESS_UPDATE_USER_PLAN.getMessage())); + + when(memberController.updateUserPlan(principal, MemberFixture.createMemberPlanUpdateRequestDTO())) + .thenReturn(result); + + mockMvc.perform(patch(DEFAULT_URL+"/plan") + .contentType(APPLICATION_JSON) + .principal(principal) + .content(objectMapper.writeValueAsString(MemberFixture.createMemberPlanUpdateRequestDTO()))) + .andDo( + document("사용자 정보 수정 성공 Example", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource(resources) + )) + .andExpect(status().isOk()); + } + + @DisplayName("유저 이름 중복체크 API 성공") + @Test + void success_checkDuplicatedName() throws Exception { + // given + val resources = ResourceSnippetParameters.builder() + .tag(TAG) + .description("닉네임 중복 체크") + .responseFields( + fieldWithPath("success").type(BOOLEAN).description("응답 성공 여부"), + fieldWithPath("message").type(STRING).description("응답 메시지"), + fieldWithPath("data").type(NULL).description("응답 데이터") + ) + .build(); + // when + when(memberController.checkDuplicatedName("test")) + .thenReturn(ResponseEntity.ok(success("닉네임 중복 체크 성공"))); + + // then + mockMvc.perform(get(DEFAULT_URL + "/nickname/check") + .contentType(APPLICATION_JSON) + .principal(principal) + .param("name", "test")) + .andDo( + document("닉네임 중복 체크 성공 Example", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource(resources) + )) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("푸시알림 동의 여부 수정 메소드 테스트") + void success_updateUserPush() throws Exception { + // given + val resources = ResourceSnippetParameters.builder() + .tag(TAG) + .description("푸시 알림 동의여부 수정") + .requestFields(fieldWithPath("hasAlarm").type(BOOLEAN).description("푸시 알림 동의여부")) + .responseFields( + fieldWithPath("success").type(BOOLEAN).description("응답 성공 여부"), + fieldWithPath("message").type(STRING).description("응답 메시지"), + fieldWithPath("data").type(NULL).description("응답 데이터") + ) + .build(); + // when + when(memberController.updateUserPush(principal, MemberFixture.createMemberPushUpdateRequestDTO())) + .thenReturn(ResponseEntity.ok(success(SUCCESS_UPDATE_USER_PUSH.getMessage()))); + + + mockMvc.perform(patch(DEFAULT_URL + "/push") + .contentType(APPLICATION_JSON) + .principal(principal) + .content(objectMapper.writeValueAsString(MemberFixture.createMemberPushUpdateRequestDTO()))) + .andDo( + document("푸시 알림 동의여부 성공 Example", + preprocessRequest(prettyPrint()), + preprocessResponse(prettyPrint()), + resource(resources) + )) + .andExpect(status().isOk()); + } +} diff --git a/src/test/java/com/smeme/server/fixture/auth/AuthFixture.java b/src/test/java/com/smeme/server/fixture/auth/AuthFixture.java new file mode 100644 index 00000000..a7424fbd --- /dev/null +++ b/src/test/java/com/smeme/server/fixture/auth/AuthFixture.java @@ -0,0 +1,44 @@ +package com.smeme.server.fixture.auth; + +import com.smeme.server.dto.auth.SignInRequestDTO; +import com.smeme.server.dto.auth.SignInResponseDTO; +import com.smeme.server.dto.auth.token.TokenResponseDTO; +import com.smeme.server.dto.auth.token.TokenVO; +import com.smeme.server.model.SocialType; + +public class AuthFixture { + + private static final SocialType SOCIAL_TYPE = SocialType.KAKAO; + private static final String FCM_TOKEN = "testfcmtoken"; + private static final String ACCESS_TOKEN = "testaccesstoken"; + private static final String REFRESH_TOKEN = "testrefreshtoken"; + + public static SignInRequestDTO createSignInRequestDTO() { + return new SignInRequestDTO( + SOCIAL_TYPE, + FCM_TOKEN + ); + } + + public static SignInResponseDTO createSignInResponseDTO() { + return SignInResponseDTO.of( + createTokenVO(), + true, + true + ); + } + + public static TokenVO createTokenVO() { + return new TokenVO( + ACCESS_TOKEN, + REFRESH_TOKEN + ); + } + + public static TokenResponseDTO createTokenResponseDTO() { + return TokenResponseDTO.of( + ACCESS_TOKEN, + REFRESH_TOKEN + ); + } +} diff --git a/src/test/java/com/smeme/server/fixture/badge/BadgeFixture.java b/src/test/java/com/smeme/server/fixture/badge/BadgeFixture.java new file mode 100644 index 00000000..3c2cfa10 --- /dev/null +++ b/src/test/java/com/smeme/server/fixture/badge/BadgeFixture.java @@ -0,0 +1,50 @@ +package com.smeme.server.fixture.badge; + + +import com.smeme.server.dto.badge.BadgeListResponseDTO; +import com.smeme.server.dto.badge.BadgeResponseDTO; +import com.smeme.server.model.badge.Badge; +import com.smeme.server.model.badge.BadgeType; + +import java.util.ArrayList; +import java.util.List; + +public class BadgeFixture { + + private static final String BADGE_NAME = "연속 3일 일기 뱃지"; + private static final Long BADGE_ID = 1L; + private static final BadgeType BADGE_TYPE = BadgeType.COMBO; + private static final String BADGE_IMAGE_URL = "https://m.s3.ap-northeast-2.amazonaws.com/badge/streak.png"; + + public static Badge createBadge() { + return Badge.builder() + .name(BADGE_NAME) + .id(BADGE_ID) + .type(BADGE_TYPE) + .imageUrl(BADGE_IMAGE_URL) + .build(); + } + + public static BadgeResponseDTO createBadgeResponseDTO() { + return BadgeResponseDTO.of(createBadge()); + } + + public static BadgeListResponseDTO createBadgeListResponseDTO() { + List badges = new ArrayList<>(); + + for (long l = 1L; l < 3L; l++) { + badges.add( + BadgeResponseDTO.of( + Badge.builder() + .id(l) + .name(BADGE_NAME) + .type(BADGE_TYPE) + .imageUrl(BADGE_IMAGE_URL) + .build() + ) + ); + } + return BadgeListResponseDTO.of(badges); + } + +} diff --git a/src/test/java/com/smeme/server/fixture/correction/CorrectionFixture.java b/src/test/java/com/smeme/server/fixture/correction/CorrectionFixture.java new file mode 100644 index 00000000..3315eed1 --- /dev/null +++ b/src/test/java/com/smeme/server/fixture/correction/CorrectionFixture.java @@ -0,0 +1,40 @@ +package com.smeme.server.fixture.correction; + +import com.smeme.server.dto.correction.CorrectionRequestDTO; +import com.smeme.server.dto.correction.CorrectionResponseDTO; +import com.smeme.server.fixture.badge.BadgeFixture; +import com.smeme.server.fixture.diary.DiaryFixture; +import com.smeme.server.fixture.member.MemberFixture; +import com.smeme.server.model.Correction; + +public class CorrectionFixture { + + private static final String CORRECTION_SENTENCE = "correction test sentence"; + private static final String CORRECTION_CONTENT = "correction test content"; + private static final String BEFORE_SENTENCE ="correction before sentence"; + private static final String AFTER_SENTENCE ="correction after sentence"; + + private static final Long DIARY_ID = 1L; + + public static Correction createCorrection() { + return Correction.builder() + .beforeSentence(BEFORE_SENTENCE) + .afterSentence(AFTER_SENTENCE) + .diary(DiaryFixture.createDiary()) + .build(); + } + + public static CorrectionResponseDTO createCorrectionResponseDTO() { + return CorrectionResponseDTO.of( + DIARY_ID, + BadgeFixture.createBadge() + ); + } + + public static CorrectionRequestDTO createCorrectionRequestDTO() { + return new CorrectionRequestDTO( + CORRECTION_SENTENCE, + CORRECTION_CONTENT + ); + } +} diff --git a/src/test/java/com/smeme/server/fixture/diary/DiaryFixture.java b/src/test/java/com/smeme/server/fixture/diary/DiaryFixture.java new file mode 100644 index 00000000..ee47fa14 --- /dev/null +++ b/src/test/java/com/smeme/server/fixture/diary/DiaryFixture.java @@ -0,0 +1,24 @@ +package com.smeme.server.fixture.diary; + +import com.smeme.server.fixture.member.MemberFixture; +import com.smeme.server.fixture.topic.TopicFixture; +import com.smeme.server.model.Diary; +import com.smeme.server.model.LangType; + +public class DiaryFixture { + private static final Long DIARY_ID = 1L; + private static final String DIARY_CONTENT = ""; + private static final LangType DIARY_LANG = LangType.en; + private static final Boolean DIARY_IS_DELETED = false; + private static final Boolean DIARY_IS_PUBLIC = true; + + public static Diary createDiary() { + return Diary.builder() + .member(MemberFixture.createMember()) + .content(DIARY_CONTENT) + .topic(TopicFixture.createTopic()) + .build(); + } + + +} diff --git a/src/test/java/com/smeme/server/fixture/goal/GoalFixture.java b/src/test/java/com/smeme/server/fixture/goal/GoalFixture.java new file mode 100644 index 00000000..8d7842d4 --- /dev/null +++ b/src/test/java/com/smeme/server/fixture/goal/GoalFixture.java @@ -0,0 +1,24 @@ +package com.smeme.server.fixture.goal; + +import com.smeme.server.dto.goal.GoalResponseDTO; +import com.smeme.server.model.goal.Goal; +import com.smeme.server.model.goal.GoalType; + +public class GoalFixture { + + private static final GoalType GOAL_TYPE = GoalType.DEVELOP; + + private static final String GOAL_WAY = "주 5회 이상 오늘 하루를 돌아보는 일기 작성하기"; + private static final String GOAL_DETAIL = "사전 없이 일기 완성\nsmeem 연속 일기 배지 획득"; + public static Goal createGoal() { + return Goal.builder() + .type(GOAL_TYPE) + .way(GOAL_WAY) + .detail(GOAL_DETAIL) + .build(); + } + + public static GoalResponseDTO createGoalResponseDTO() { + return GoalResponseDTO.of(createGoal()); + } +} diff --git a/src/test/java/com/smeme/server/fixture/member/MemberFixture.java b/src/test/java/com/smeme/server/fixture/member/MemberFixture.java new file mode 100644 index 00000000..82c2a919 --- /dev/null +++ b/src/test/java/com/smeme/server/fixture/member/MemberFixture.java @@ -0,0 +1,95 @@ +package com.smeme.server.fixture.member; + +import com.smeme.server.dto.member.*; +import com.smeme.server.fixture.badge.BadgeFixture; +import com.smeme.server.fixture.goal.GoalFixture; +import com.smeme.server.fixture.trainingtime.TrainingTimeFixture; +import com.smeme.server.model.LangType; +import com.smeme.server.model.Member; +import com.smeme.server.model.SocialType; +import com.smeme.server.model.badge.Badge; +import com.smeme.server.model.badge.BadgeType; +import com.smeme.server.model.goal.GoalType; +import lombok.val; + +import java.util.ArrayList; +import java.util.List; + +import static com.smeme.server.fixture.badge.BadgeFixture.createBadgeResponseDTO; +import static com.smeme.server.fixture.goal.GoalFixture.createGoalResponseDTO; + +public class MemberFixture { + + private static final String MEMBER_USERNAME = "홍길동"; + + private static final SocialType MEMBER_SOCIAL = SocialType.KAKAO; + private static final String MEMBER_SOCIAL_ID = "123456789"; + private static final String MEMBER_FCM_TOKEN = "fcm_token"; + private static final LangType TARGET_LANG = LangType.en; + private static final boolean MEMBER_IS_EXIST = true; + + + private static final boolean HAS_ALARM = true; + + // Badge + private static final String BADGE_NAME = "연속 3일 일기 뱃지"; + private static final Long BADGE_ID = 1L; + private static final BadgeType BADGE_TYPE = BadgeType.COMBO; + private static final String BADGE_IMAGE_URL = "https://m.s3.ap-northeast-2.amazonaws.com/badge/streak.png"; + + public static Member createMember() { + val member = Member.builder() + .social(MEMBER_SOCIAL) + .socialId(MEMBER_SOCIAL_ID) + .targetLang(TARGET_LANG) + .fcmToken(MEMBER_FCM_TOKEN) + .build(); + member.updateGoal(GoalType.DEVELOP); + return member; + } + + public static MemberGetResponseDTO createMemberGetResponseDTO() { + return MemberGetResponseDTO.of( + GoalFixture.createGoalResponseDTO(), + createMember(), + TrainingTimeFixture.createTrainingTimeResponseDTO(), + BadgeFixture.createBadgeResponseDTO() + ); + } + + public static MemberNameResponseDTO createMemberNameResponseDTO() { + return new MemberNameResponseDTO(MEMBER_IS_EXIST); + } + + public static MemberUpdateRequestDTO createMemberUpdateRequestDTO() { + return new MemberUpdateRequestDTO(MEMBER_USERNAME, HAS_ALARM); + } + + public static MemberPlanUpdateRequestDTO createMemberPlanUpdateRequestDTO() { + return new MemberPlanUpdateRequestDTO( + GoalType.DEVELOP, + TrainingTimeFixture.createTrainingTimeRequestDTO(), + HAS_ALARM + ); + } + + public static MemberUpdateResponseDTO createMemberUpdateResponseDTO() { + + List badges = new ArrayList<>(); + + for (long l = 1L; l < 5L; l++) { + Badge badge = Badge.builder() + .id(l) + .name(BADGE_NAME) + .type(BADGE_TYPE) + .imageUrl(BADGE_IMAGE_URL) + .build(); + badges.add(badge); + } + return MemberUpdateResponseDTO.of(badges); + } + + public static MemberPushUpdateRequestDTO createMemberPushUpdateRequestDTO() { + return new MemberPushUpdateRequestDTO(HAS_ALARM); + } +} \ No newline at end of file diff --git a/src/test/java/com/smeme/server/fixture/memberbadge/MemberBadgeFixture.java b/src/test/java/com/smeme/server/fixture/memberbadge/MemberBadgeFixture.java new file mode 100644 index 00000000..cf96abb9 --- /dev/null +++ b/src/test/java/com/smeme/server/fixture/memberbadge/MemberBadgeFixture.java @@ -0,0 +1,4 @@ +package com.smeme.server.fixture.memberbadge; + +public class MemberBadgeFixture { +} diff --git a/src/test/java/com/smeme/server/fixture/topic/TopicFixture.java b/src/test/java/com/smeme/server/fixture/topic/TopicFixture.java new file mode 100644 index 00000000..8c8f971b --- /dev/null +++ b/src/test/java/com/smeme/server/fixture/topic/TopicFixture.java @@ -0,0 +1,23 @@ +package com.smeme.server.fixture.topic; + +import com.smeme.server.dto.topic.TopicResponseDTO; +import com.smeme.server.model.topic.Category; +import com.smeme.server.model.topic.Topic; + +public class TopicFixture { + + private static final Category TOPIC_CATEGORY = Category.DEVELOP; + private static final String TOPIC_CONTENT = "test topic content"; + + + public static Topic createTopic() { + return Topic.builder() + .category(TOPIC_CATEGORY) + .content(TOPIC_CONTENT) + .build(); + } + + public static TopicResponseDTO createTopicResponseDTO() { + return TopicResponseDTO.of(createTopic()); + } +} diff --git a/src/test/java/com/smeme/server/fixture/trainingtime/TrainingTimeFixture.java b/src/test/java/com/smeme/server/fixture/trainingtime/TrainingTimeFixture.java new file mode 100644 index 00000000..347840b3 --- /dev/null +++ b/src/test/java/com/smeme/server/fixture/trainingtime/TrainingTimeFixture.java @@ -0,0 +1,32 @@ +package com.smeme.server.fixture.trainingtime; + +import com.smeme.server.dto.training.TrainingTimeRequestDTO; +import com.smeme.server.dto.training.TrainingTimeResponseDTO; +import com.smeme.server.fixture.member.MemberFixture; +import com.smeme.server.model.training.DayType; +import com.smeme.server.model.training.TrainingTime; + +public class TrainingTimeFixture { + + private static final DayType DAY_TYPE = DayType.MON; + private static final int HOUR = 10; + private static final int MINUTE = 30; + + public static TrainingTimeRequestDTO createTrainingTimeRequestDTO() { + return new TrainingTimeRequestDTO(DAY_TYPE.name(), HOUR, MINUTE); + } + + public static TrainingTime createTrainingTime() { + return TrainingTime.builder() + .day(DAY_TYPE) + .hour(HOUR) + .minute(MINUTE) + .member(MemberFixture.createMember()) + .build(); + } + + public static TrainingTimeResponseDTO createTrainingTimeResponseDTO() { + return TrainingTimeResponseDTO.of(DAY_TYPE.name(), HOUR, MINUTE); + } + +}