diff --git a/backend/.gitignore b/backend/.gitignore index c2065bc2..324c82fa 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -35,3 +35,9 @@ out/ ### VS Code ### .vscode/ + +### Mac ### +.DS_Store + +### RestDocs ### +openapi3.yaml diff --git a/backend/build.gradle b/backend/build.gradle index eb6588c4..82fe6e8f 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -2,6 +2,7 @@ plugins { id 'java' id 'org.springframework.boot' version '3.3.1' id 'io.spring.dependency-management' version '1.1.5' + id 'com.epages.restdocs-api-spec' version '0.18.2' } group = 'com.zzang' @@ -33,9 +34,37 @@ dependencies { runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'com.epages:restdocs-api-spec-mockmvc:0.18.2' + testImplementation 'com.epages:restdocs-api-spec-restassured:0.18.2' + testImplementation 'io.rest-assured:rest-assured:5.3.1' + testImplementation 'org.springframework.restdocs:spring-restdocs-restassured' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } -tasks.named('test') { +// test -> openapi3 -> copyOasToSwagger -> bootJar + +test { useJUnitPlatform() } + +openapi3 { + servers = [{ url = 'http://13.124.137.6' }, + { url = 'http://localhost:8080' }] + title '총대마켓 API 명세서' + description '총대마켓 백엔드 API 명세서' + version '0.1.0' + format 'yaml' +} + +tasks.register('copyOasToSwagger', Copy) { + dependsOn('openapi3') + doFirst { + delete file('src/main/resources/static/swagger-ui/openapi3.yaml') + } + from file("build/api-spec/openapi3.yaml") + into file("src/main/resources/static/swagger-ui/.") +} + +bootJar { + dependsOn copyOasToSwagger +} diff --git a/backend/http/comment.http b/backend/http/comment.http new file mode 100644 index 00000000..fb8ce614 --- /dev/null +++ b/backend/http/comment.http @@ -0,0 +1,2 @@ +### 댓글방 목록 조회 API +GET {{base-url}}/comments?member-id=1 diff --git a/backend/src/main/java/com/zzang/chongdae/comment/controller/CommentController.java b/backend/src/main/java/com/zzang/chongdae/comment/controller/CommentController.java index 7be31840..0d067255 100644 --- a/backend/src/main/java/com/zzang/chongdae/comment/controller/CommentController.java +++ b/backend/src/main/java/com/zzang/chongdae/comment/controller/CommentController.java @@ -4,8 +4,6 @@ import com.zzang.chongdae.comment.service.dto.CommentAllResponse; import com.zzang.chongdae.comment.service.dto.CommentRoomAllResponse; import com.zzang.chongdae.comment.service.dto.CommentSaveRequest; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -15,14 +13,12 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -@Tag(name = "Comment(댓글)") @RequiredArgsConstructor @RestController public class CommentController { private final CommentService commentService; - @Operation(summary = "댓글 작성", description = "댓글을 작성합니다.") @PostMapping("/comments") public ResponseEntity saveComment( @RequestBody CommentSaveRequest commentSaveRequest) { @@ -30,7 +26,6 @@ public ResponseEntity saveComment( return ResponseEntity.ok().build(); } - @Operation(summary = "댓글방 목록 조회", description = "댓글방 목록을 조회합니다.") @GetMapping("/comments") public ResponseEntity getAllCommentRoom( @RequestParam(value = "member-id") Long loginMemberId) { @@ -38,7 +33,6 @@ public ResponseEntity getAllCommentRoom( return ResponseEntity.ok(response); } - @Operation(summary = "댓글 목록 조회", description = "댓글 목록을 조회합니다.") @GetMapping("/comments/{offering-id}") public ResponseEntity getAllComment( @PathVariable(value = "offering-id") Long offeringId, diff --git a/backend/src/main/java/com/zzang/chongdae/global/config/StaticRoutingConfig.java b/backend/src/main/java/com/zzang/chongdae/global/config/StaticRoutingConfig.java new file mode 100644 index 00000000..2f4f8619 --- /dev/null +++ b/backend/src/main/java/com/zzang/chongdae/global/config/StaticRoutingConfig.java @@ -0,0 +1,14 @@ +package com.zzang.chongdae.global.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class StaticRoutingConfig implements WebMvcConfigurer { + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/"); + } +} diff --git a/backend/src/main/java/com/zzang/chongdae/member/repository/entity/MemberEntity.java b/backend/src/main/java/com/zzang/chongdae/member/repository/entity/MemberEntity.java index 9218ed0b..881c53b9 100644 --- a/backend/src/main/java/com/zzang/chongdae/member/repository/entity/MemberEntity.java +++ b/backend/src/main/java/com/zzang/chongdae/member/repository/entity/MemberEntity.java @@ -30,6 +30,10 @@ public class MemberEntity extends BaseTimeEntity { @Column(unique = true) private String nickname; + public MemberEntity(String nickname) { + this(null, nickname); + } + public boolean isSameMember(Long memberId) { return this.id.equals(memberId); } diff --git a/backend/src/main/java/com/zzang/chongdae/offering/controller/OfferingController.java b/backend/src/main/java/com/zzang/chongdae/offering/controller/OfferingController.java index d0139b53..9a51c7eb 100644 --- a/backend/src/main/java/com/zzang/chongdae/offering/controller/OfferingController.java +++ b/backend/src/main/java/com/zzang/chongdae/offering/controller/OfferingController.java @@ -4,23 +4,19 @@ import com.zzang.chongdae.offering.service.dto.OfferingAllResponse; import com.zzang.chongdae.offering.service.dto.OfferingDetailResponse; import com.zzang.chongdae.offering.service.dto.OfferingMeetingResponse; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; -@Tag(name = "Offerring(공모)") @RequiredArgsConstructor -@Controller +@RestController public class OfferingController { private final OfferingService offeringService; - @Operation(summary = "공모 상세 조회", description = "공모 id를 통해 공모의 상세 정보를 조회합니다.") @GetMapping("/offerings/{offering-id}") public ResponseEntity getOfferingDetail( @PathVariable(value = "offering-id") Long offeringId, @RequestParam(value = "member-id") Long memberId) { @@ -28,7 +24,6 @@ public ResponseEntity getOfferingDetail( return ResponseEntity.ok(response); } - @Operation(summary = "공모 목록 조회", description = "공모 목록을 조회합니다.") @GetMapping("/offerings") public ResponseEntity getAllOffering( @RequestParam(value = "last-id", defaultValue = "0") Long lastId, @@ -37,7 +32,6 @@ public ResponseEntity getAllOffering( return ResponseEntity.ok(response); } - @Operation(summary = "공모 일정 조회", description = "공모 id를 통해 공모의 일정 정보를 조회합니다.") @GetMapping("/offerings/{offering-id}/meetings") public ResponseEntity getOfferingMeeting( @PathVariable(value = "offering-id") Long offeringId) { diff --git a/backend/src/main/java/com/zzang/chongdae/offering/repository/entity/OfferingEntity.java b/backend/src/main/java/com/zzang/chongdae/offering/repository/entity/OfferingEntity.java index 7622e007..cf3e856e 100644 --- a/backend/src/main/java/com/zzang/chongdae/offering/repository/entity/OfferingEntity.java +++ b/backend/src/main/java/com/zzang/chongdae/offering/repository/entity/OfferingEntity.java @@ -69,6 +69,13 @@ public class OfferingEntity extends BaseTimeEntity { @NotNull private BigDecimal totalPrice; + public OfferingEntity(MemberEntity member, String title, String description, String thumbnailUrl, String productUrl, + LocalDateTime deadline, String meetingAddress, String meetingAddressDetail, + Integer totalCount, Integer currentCount, Boolean isManualConfirmed, BigDecimal totalPrice) { + this(null, member, title, description, thumbnailUrl, productUrl, deadline, meetingAddress, + meetingAddressDetail, totalCount, currentCount, isManualConfirmed, totalPrice); + } + public void updateCurrentCount() { currentCount++; } diff --git a/backend/src/main/java/com/zzang/chongdae/offeringmember/controller/OfferingMemberController.java b/backend/src/main/java/com/zzang/chongdae/offeringmember/controller/OfferingMemberController.java index 4e20567a..ef336ea5 100644 --- a/backend/src/main/java/com/zzang/chongdae/offeringmember/controller/OfferingMemberController.java +++ b/backend/src/main/java/com/zzang/chongdae/offeringmember/controller/OfferingMemberController.java @@ -2,23 +2,19 @@ import com.zzang.chongdae.offeringmember.service.OfferingMemberService; import com.zzang.chongdae.offeringmember.service.dto.ParticipationRequest; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; import java.net.URI; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; -@Tag(name = "OfferingMember(공모인원)") @RequiredArgsConstructor -@Controller +@RestController public class OfferingMemberController { private final OfferingMemberService offeringMemberService; - @Operation(summary = "공모 참여", description = "게시된 공모에 참여합니다.") @PostMapping("/participations") ResponseEntity participate(@RequestBody ParticipationRequest participationRequest) { Long id = offeringMemberService.participate(participationRequest); diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 42440f3c..57271c2f 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -15,8 +15,6 @@ spring: hibernate: format_sql: true springdoc: - packages-to-scan: com.zzang.chongdae - default-consumes-media-type: application/json;charset=UTF-8 - default-produces-media-type: application/json;charset=UTF-8 swagger-ui: path: swagger-ui.html + url: /static/swagger-ui/openapi3.yaml diff --git a/backend/src/test/java/com/zzang/chongdae/comment/integration/CommentIntegrationTest.java b/backend/src/test/java/com/zzang/chongdae/comment/integration/CommentIntegrationTest.java new file mode 100644 index 00000000..a2286f31 --- /dev/null +++ b/backend/src/test/java/com/zzang/chongdae/comment/integration/CommentIntegrationTest.java @@ -0,0 +1,163 @@ +package com.zzang.chongdae.comment.integration; + +import static com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static com.epages.restdocs.apispec.ResourceSnippetParameters.builder; +import static com.epages.restdocs.apispec.RestAssuredRestDocumentationWrapper.document; +import static com.epages.restdocs.apispec.Schema.schema; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; + +import com.epages.restdocs.apispec.ParameterDescriptorWithType; +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.zzang.chongdae.global.integration.IntegrationTest; +import com.zzang.chongdae.member.repository.entity.MemberEntity; +import com.zzang.chongdae.offering.repository.entity.OfferingEntity; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import java.util.List; +import java.util.Map; +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.restdocs.payload.FieldDescriptor; + +public class CommentIntegrationTest extends IntegrationTest { + + @DisplayName("댓글 작성") + @Nested + class SaveComment { + + List saveCommentRequestDescriptors = List.of( + fieldWithPath("memberId").description("회원 id"), + fieldWithPath("offeringId").description("공모 id"), + fieldWithPath("content").description("내용") + ); + ResourceSnippetParameters snippets = builder() + .summary("댓글 작성") + .description("댓글을 작성합니다.") + .requestFields(saveCommentRequestDescriptors) + .requestSchema(schema("CommentSaveRequest")) + .build(); + MemberEntity member; + OfferingEntity offering; + + @BeforeEach + void setUp() { + member = memberFixture.createMember(); + offering = offeringFixture.createOffering(member); + } + + @DisplayName("댓글을 작성할 수 있다") + @Test + void should_saveCommentSuccess_when_givenCommentSaveRequest() { + Map request = Map.of( + "memberId", member.getId().toString(), + "offeringId", offering.getId().toString(), + "content", "댓글 내용" + ); + + RestAssured.given(spec).log().all() + .filter(document("save-comment-success", resource(snippets))) + .contentType(ContentType.JSON) + .body(request) + .when().post("/comments") + .then().log().all() + .statusCode(200); + } + } + + @DisplayName("댓글방 목록 조회") + @Nested + class GetAllCommentRoom { + + List getAllCommentRoomQueryParameterDescriptors = List.of( + parameterWithName("member-id").description("회원 id") + ); + List getAllCommentRoomResponseDescriptors = List.of( + fieldWithPath("offerings[].offeringId").description("공모 id"), + fieldWithPath("offerings[].offeringTitle").description("공모 제목"), + fieldWithPath("offerings[].latestComment.content").description("최신 댓글 내용"), + fieldWithPath("offerings[].latestComment.createdAt").description("최신 댓글 작성일"), + fieldWithPath("offerings[].isProposer").description("총대 여부") + ); + ResourceSnippetParameters snippets = builder() + .summary("댓글방 목록 조회") + .description("댓글방 목록을 조회합니다.") + .queryParameters(getAllCommentRoomQueryParameterDescriptors) + .responseFields(getAllCommentRoomResponseDescriptors) + .responseSchema(schema("CommentRoomAllResponse")) + .build(); + MemberEntity member; + OfferingEntity offering; + + @BeforeEach + void setUp() { + member = memberFixture.createMember(); + offering = offeringFixture.createOffering(member); + offeringMemberFixture.createProposer(member, offering); + commentFixture.createComment(member, offering); + } + + @DisplayName("댓글방 목록을 조회할 수 있다") + @Test + void should_responseAllCommentRoom_when_givenMemberId() { + RestAssured.given(spec).log().all() + .filter(document("get-all-comment-room-success", resource(snippets))) + .queryParam("member-id", member.getId()) + .when().get("/comments") + .then().log().all() + .statusCode(200); + } + } + + @DisplayName("댓글 목록 조회") + @Nested + class GetAllComment { + + List getAllCommentPathParameterDescriptors = List.of( + parameterWithName("offering-id").description("공모 id") + ); + List getAllCommentQueryParameterDescriptors = List.of( + parameterWithName("member-id").description("회원 id") + ); + List getAllCommentResponseDescriptors = List.of( + fieldWithPath("comments[].createdAt.date").description("작성 날짜"), + fieldWithPath("comments[].createdAt.time").description("작성 시간"), + fieldWithPath("comments[].content").description("내용"), + fieldWithPath("comments[].nickname").description("작성자 회원 닉네임"), + fieldWithPath("comments[].isProposer").description("총대 여부"), + fieldWithPath("comments[].isMine").description("내 댓글 여부") + ); + ResourceSnippetParameters snippets = builder() + .summary("댓글 목록 조회") + .description("댓글 목록을 조회합니다.") + .pathParameters(getAllCommentPathParameterDescriptors) + .queryParameters(getAllCommentQueryParameterDescriptors) + .responseFields(getAllCommentResponseDescriptors) + .responseSchema(schema("CommentAllResponse")) + .build(); + MemberEntity member; + OfferingEntity offering; + + @BeforeEach + void setUp() { + member = memberFixture.createMember(); + offering = offeringFixture.createOffering(member); + offeringMemberFixture.createProposer(member, offering); + commentFixture.createComment(member, offering); + } + + @DisplayName("댓글 목록을 조회할 수 있다") + @Test + void should_responseAllComment_when_givenOfferingIdAndMemberId() { + RestAssured.given(spec).log().all() + .filter(document("get-all-comment-success", resource(snippets))) + .pathParam("offering-id", offering.getId()) + .queryParam("member-id", member.getId()) + .when().get("/comments/{offering-id}") + .then().log().all() + .statusCode(200); + } + } +} diff --git a/backend/src/test/java/com/zzang/chongdae/global/domain/CommentFixture.java b/backend/src/test/java/com/zzang/chongdae/global/domain/CommentFixture.java new file mode 100644 index 00000000..3a04fac4 --- /dev/null +++ b/backend/src/test/java/com/zzang/chongdae/global/domain/CommentFixture.java @@ -0,0 +1,24 @@ +package com.zzang.chongdae.global.domain; + +import com.zzang.chongdae.comment.repository.CommentRepository; +import com.zzang.chongdae.comment.repository.entity.CommentEntity; +import com.zzang.chongdae.member.repository.entity.MemberEntity; +import com.zzang.chongdae.offering.repository.entity.OfferingEntity; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class CommentFixture { + + @Autowired + private CommentRepository commentRepository; + + public CommentEntity createComment(MemberEntity member, OfferingEntity offering) { + CommentEntity comment = new CommentEntity( + member, + offering, + "안녕하세요" + ); + return commentRepository.save(comment); + } +} diff --git a/backend/src/test/java/com/zzang/chongdae/global/domain/DomainSupplier.java b/backend/src/test/java/com/zzang/chongdae/global/domain/DomainSupplier.java new file mode 100644 index 00000000..8546001d --- /dev/null +++ b/backend/src/test/java/com/zzang/chongdae/global/domain/DomainSupplier.java @@ -0,0 +1,20 @@ +package com.zzang.chongdae.global.domain; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public abstract class DomainSupplier { + + @Autowired + protected MemberFixture memberFixture; + + @Autowired + protected OfferingFixture offeringFixture; + + @Autowired + protected OfferingMemberFixture offeringMemberFixture; + + @Autowired + protected CommentFixture commentFixture; +} diff --git a/backend/src/test/java/com/zzang/chongdae/global/domain/MemberFixture.java b/backend/src/test/java/com/zzang/chongdae/global/domain/MemberFixture.java new file mode 100644 index 00000000..b10e9196 --- /dev/null +++ b/backend/src/test/java/com/zzang/chongdae/global/domain/MemberFixture.java @@ -0,0 +1,23 @@ +package com.zzang.chongdae.global.domain; + +import com.zzang.chongdae.member.repository.MemberRepository; +import com.zzang.chongdae.member.repository.entity.MemberEntity; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class MemberFixture { + + @Autowired + private MemberRepository memberRepository; + + public MemberEntity createMember() { + MemberEntity member = new MemberEntity("dora"); + return memberRepository.save(member); + } + + public MemberEntity createMember(String nickname) { + MemberEntity member = new MemberEntity(nickname); + return memberRepository.save(member); + } +} diff --git a/backend/src/test/java/com/zzang/chongdae/global/domain/OfferingFixture.java b/backend/src/test/java/com/zzang/chongdae/global/domain/OfferingFixture.java new file mode 100644 index 00000000..0f740727 --- /dev/null +++ b/backend/src/test/java/com/zzang/chongdae/global/domain/OfferingFixture.java @@ -0,0 +1,34 @@ +package com.zzang.chongdae.global.domain; + +import com.zzang.chongdae.member.repository.entity.MemberEntity; +import com.zzang.chongdae.offering.repository.OfferingRepository; +import com.zzang.chongdae.offering.repository.entity.OfferingEntity; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class OfferingFixture { + + @Autowired + private OfferingRepository offeringRepository; + + public OfferingEntity createOffering(MemberEntity member) { + OfferingEntity offering = new OfferingEntity( + member, + "title", + "description", + "thumbnailUrl", + "productUrl", + LocalDateTime.of(3000, 1, 1, 0, 0, 0), + "meetingAddress", + "meetingAddressDetail", + 5, + 1, + false, + BigDecimal.valueOf(5000) + ); + return offeringRepository.save(offering); + } +} diff --git a/backend/src/test/java/com/zzang/chongdae/global/domain/OfferingMemberFixture.java b/backend/src/test/java/com/zzang/chongdae/global/domain/OfferingMemberFixture.java new file mode 100644 index 00000000..44ddfea6 --- /dev/null +++ b/backend/src/test/java/com/zzang/chongdae/global/domain/OfferingMemberFixture.java @@ -0,0 +1,34 @@ +package com.zzang.chongdae.global.domain; + +import com.zzang.chongdae.member.repository.entity.MemberEntity; +import com.zzang.chongdae.offering.repository.entity.OfferingEntity; +import com.zzang.chongdae.offeringmember.domain.OfferingMemberRole; +import com.zzang.chongdae.offeringmember.repository.OfferingMemberRepository; +import com.zzang.chongdae.offeringmember.repository.entity.OfferingMemberEntity; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class OfferingMemberFixture { + + @Autowired + private OfferingMemberRepository offeringMemberRepository; + + public OfferingMemberEntity createProposer(MemberEntity member, OfferingEntity offering) { + OfferingMemberEntity offeringMember = new OfferingMemberEntity( + member, + offering, + OfferingMemberRole.PROPOSER + ); + return offeringMemberRepository.save(offeringMember); + } + + public OfferingMemberEntity createParticipant(MemberEntity member, OfferingEntity offering) { + OfferingMemberEntity offeringMember = new OfferingMemberEntity( + member, + offering, + OfferingMemberRole.PARTICIPANT + ); + return offeringMemberRepository.save(offeringMember); + } +} diff --git a/backend/src/test/java/com/zzang/chongdae/global/helper/DatabaseCleaner.java b/backend/src/test/java/com/zzang/chongdae/global/helper/DatabaseCleaner.java new file mode 100644 index 00000000..447b2521 --- /dev/null +++ b/backend/src/test/java/com/zzang/chongdae/global/helper/DatabaseCleaner.java @@ -0,0 +1,43 @@ +package com.zzang.chongdae.global.helper; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +@Transactional +public class DatabaseCleaner { + + @PersistenceContext + private EntityManager entityManager; + + public void execute() { // TODO: 테이블명 빼와서 반복문으로 돌리기 + entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate(); + clearMember(); + clearOfferingMember(); + clearOffering(); + clearComment(); + entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate(); + } + + private void clearMember() { + entityManager.createNativeQuery("DELETE FROM member").executeUpdate(); + entityManager.createNativeQuery("ALTER TABLE member ALTER COLUMN id RESTART").executeUpdate(); + } + + private void clearOfferingMember() { + entityManager.createNativeQuery("DELETE FROM offering_member").executeUpdate(); + entityManager.createNativeQuery("ALTER TABLE offering_member ALTER COLUMN id RESTART").executeUpdate(); + } + + private void clearOffering() { + entityManager.createNativeQuery("DELETE FROM offering").executeUpdate(); + entityManager.createNativeQuery("ALTER TABLE offering ALTER COLUMN id RESTART").executeUpdate(); + } + + private void clearComment() { + entityManager.createNativeQuery("DELETE FROM comment").executeUpdate(); + entityManager.createNativeQuery("ALTER TABLE comment ALTER COLUMN id RESTART").executeUpdate(); + } +} diff --git a/backend/src/test/java/com/zzang/chongdae/global/integration/IntegrationTest.java b/backend/src/test/java/com/zzang/chongdae/global/integration/IntegrationTest.java new file mode 100644 index 00000000..36b299ee --- /dev/null +++ b/backend/src/test/java/com/zzang/chongdae/global/integration/IntegrationTest.java @@ -0,0 +1,56 @@ +package com.zzang.chongdae.global.integration; + +import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; +import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; +import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.documentationConfiguration; + +import com.zzang.chongdae.global.domain.DomainSupplier; +import com.zzang.chongdae.global.helper.DatabaseCleaner; +import io.restassured.RestAssured; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.specification.RequestSpecification; +import org.junit.jupiter.api.BeforeEach; +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.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.RestDocumentationExtension; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@ActiveProfiles("test") +@ExtendWith(RestDocumentationExtension.class) +public abstract class IntegrationTest extends DomainSupplier { + + @LocalServerPort + private int port; + + protected RequestSpecification spec; + + @Autowired + protected DatabaseCleaner databaseCleaner; + + @BeforeEach + protected void setUp(RestDocumentationContextProvider restDocumentation) { + RestAssured.port = port; + spec = new RequestSpecBuilder() + .addFilter(documentationConfiguration(restDocumentation) + .operationPreprocessors() + .withRequestDefaults(prettyPrint(), modifyHeaders() + .remove("Host") + .remove("Content-Length") + ) + .withResponseDefaults(prettyPrint(), modifyHeaders() + .remove("Transfer-Encoding") + .remove("Keep-Alive") + .remove("Date") + .remove("Connection") + .remove("Content-Length") + ) + ) + .build(); + databaseCleaner.execute(); + } +} diff --git a/backend/src/test/java/com/zzang/chongdae/offering/integration/OfferingIntegrationTest.java b/backend/src/test/java/com/zzang/chongdae/offering/integration/OfferingIntegrationTest.java new file mode 100644 index 00000000..4fc84a3c --- /dev/null +++ b/backend/src/test/java/com/zzang/chongdae/offering/integration/OfferingIntegrationTest.java @@ -0,0 +1,165 @@ +package com.zzang.chongdae.offering.integration; + +import static com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName; +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static com.epages.restdocs.apispec.ResourceSnippetParameters.builder; +import static com.epages.restdocs.apispec.RestAssuredRestDocumentationWrapper.document; +import static com.epages.restdocs.apispec.Schema.schema; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; + +import com.epages.restdocs.apispec.ParameterDescriptorWithType; +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.zzang.chongdae.global.integration.IntegrationTest; +import com.zzang.chongdae.member.repository.entity.MemberEntity; +import io.restassured.RestAssured; +import java.util.List; +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.restdocs.payload.FieldDescriptor; + +public class OfferingIntegrationTest extends IntegrationTest { + + @DisplayName("공모 상세 조회") + @Nested + class GetOfferingDetail { + + List offeringDetailPathParameterDescriptors = List.of( + parameterWithName("offering-id").description("공모 id") + ); + List offeringDetailQueryParameterDescriptors = List.of( + parameterWithName("member-id").description("회원 id") + ); + List offeringDetailResponseDescriptors = List.of( + fieldWithPath("id").description("공모 id"), + fieldWithPath("title").description("제목"), + fieldWithPath("productUrl").description("물품 링크"), + fieldWithPath("meetingAddress").description("모집 주소"), + fieldWithPath("meetingAddressDetail").description("모집 상세 주소"), + fieldWithPath("description").description("내용"), + fieldWithPath("deadline").description("마감시간"), + fieldWithPath("currentCount").description("현재원"), + fieldWithPath("totalCount").description("총원"), + fieldWithPath("thumbnailUrl").description("사진 링크"), + fieldWithPath("dividedPrice").description("n빵 가격"), + fieldWithPath("totalPrice").description("총가격"), + fieldWithPath("condition").description("공모 상태"), + fieldWithPath("memberId").description("공모자 회원 id"), + fieldWithPath("nickname").description("공모자 회원 닉네임"), + fieldWithPath("isParticipated").description("공모 참여 여부") + ); + ResourceSnippetParameters snippets = builder() + .summary("공모 상세 조회") + .description("공모 id를 통해 공모의 상세 정보를 조회합니다.") + .pathParameters(offeringDetailPathParameterDescriptors) + .queryParameters(offeringDetailQueryParameterDescriptors) + .responseFields(offeringDetailResponseDescriptors) + .responseSchema(schema("OfferingDetailResponse")) + .build(); + + @BeforeEach + void setUp() { + MemberEntity member = memberFixture.createMember(); + offeringFixture.createOffering(member); + } + + @DisplayName("공모 id로 공모 상세 정보를 조회할 수 있다") + @Test + void should_responseOfferingDetail_when_givenOfferingId() { + RestAssured.given(spec).log().all() + .filter(document("get-offering-detail-success", resource(snippets))) + .pathParam("offering-id", 1) + .queryParam("member-id", 1) + .when().get("/offerings/{offering-id}") + .then().log().all() + .statusCode(200); + } + } + + @DisplayName("공모 목록 조회") + @Nested + class GetAllOffering { + + List offeringAllQueryParameterDescriptors = List.of( + parameterWithName("last-id").description("마지막 공모 id"), + parameterWithName("page-size").description("페이지 크기") + ); + List offeringAllResponseDescriptors = List.of( + fieldWithPath("offerings[].id").description("공모 id"), + fieldWithPath("offerings[].title").description("제목"), + fieldWithPath("offerings[].meetingAddress").description("모집 주소"), + fieldWithPath("offerings[].currentCount").description("현재원"), + fieldWithPath("offerings[].totalCount").description("총원"), + fieldWithPath("offerings[].thumbnailUrl").description("사진 링크"), + fieldWithPath("offerings[].dividedPrice").description("n빵 가격"), + fieldWithPath("offerings[].condition").description("공모 상태"), + fieldWithPath("offerings[].isOpen").description("공모 참여 가능 여부") + ); + ResourceSnippetParameters snippets = builder() + .summary("공모 목록 조회") + .description("공모 목록을 조회합니다.") + .queryParameters(offeringAllQueryParameterDescriptors) + .responseFields(offeringAllResponseDescriptors) + .responseSchema(schema("OfferingAllResponse")) + .build(); + + @BeforeEach + void setUp() { + MemberEntity member = memberFixture.createMember(); + for (int i = 0; i < 11; i++) { + offeringFixture.createOffering(member); + } + } + + @DisplayName("공모 목록을 조회할 수 있다") + @Test + void should_responseAllOffering_when_givenPageInfo() { + RestAssured.given(spec).log().all() + .filter(document("get-all-offering-success", resource(snippets))) + .queryParam("last-id", 1) + .queryParam("page-size", 10) + .when().get("/offerings") + .then().log().all() + .statusCode(200); + } + } + + @DisplayName("공모 일정 조회") + @Nested + class GetOfferingMeeting { + + List offeringMeetingPathParameterDescriptors = List.of( + parameterWithName("offering-id").description("공모 id") + ); + List offeringMeetingResponseDescriptors = List.of( + fieldWithPath("deadline").description("마감시간"), + fieldWithPath("meetingAddress").description("모집 주소"), + fieldWithPath("meetingAddressDetail").description("모집 상세 주소") + ); + ResourceSnippetParameters snippets = builder() + .summary("공모 일정 조회") + .description("공모 id를 통해 공모의 일정 정보를 조회합니다.") + .pathParameters(offeringMeetingPathParameterDescriptors) + .responseFields(offeringMeetingResponseDescriptors) + .responseSchema(schema("OfferingMeetingResponse")) + .build(); + + @BeforeEach + void setUp() { + MemberEntity member = memberFixture.createMember(); + offeringFixture.createOffering(member); + } + + @DisplayName("공모 id로 공모 일정 정보를 조회할 수 있다") + @Test + void should_responseOfferingMeeting_when_givenOfferingId() { + RestAssured.given(spec).log().all() + .filter(document("get-offering-meeting-success", resource(snippets))) + .pathParam("offering-id", 1) + .when().get("/offerings/{offering-id}/meetings") + .then().log().all() + .statusCode(200); + } + } +} diff --git a/backend/src/test/java/com/zzang/chongdae/offeringmember/integration/OfferingMemberIntegrationTest.java b/backend/src/test/java/com/zzang/chongdae/offeringmember/integration/OfferingMemberIntegrationTest.java new file mode 100644 index 00000000..83daedaa --- /dev/null +++ b/backend/src/test/java/com/zzang/chongdae/offeringmember/integration/OfferingMemberIntegrationTest.java @@ -0,0 +1,84 @@ +package com.zzang.chongdae.offeringmember.integration; + +import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static com.epages.restdocs.apispec.RestAssuredRestDocumentationWrapper.document; +import static com.epages.restdocs.apispec.Schema.schema; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; + +import com.epages.restdocs.apispec.ResourceSnippetParameters; +import com.zzang.chongdae.global.integration.IntegrationTest; +import com.zzang.chongdae.member.repository.entity.MemberEntity; +import com.zzang.chongdae.offering.repository.entity.OfferingEntity; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import java.util.List; +import java.util.Map; +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.restdocs.payload.FieldDescriptor; + +public class OfferingMemberIntegrationTest extends IntegrationTest { + + @DisplayName("공모 참여") + @Nested + class Participate { + + List participationRequestDescriptors = List.of( + fieldWithPath("memberId").description("회원 id"), + fieldWithPath("offeringId").description("공모 id") + ); + ResourceSnippetParameters snippets = ResourceSnippetParameters.builder() + .summary("공모 참여") + .description("게시된 공모에 참여합니다.") + .requestFields(participationRequestDescriptors) + .requestSchema(schema("ParticipationRequest")) + .build(); + MemberEntity proposer; + MemberEntity participant; + OfferingEntity offering; + + @BeforeEach + void setUp() { + proposer = memberFixture.createMember("dora"); + participant = memberFixture.createMember("poke"); + offering = offeringFixture.createOffering(proposer); + offeringMemberFixture.createProposer(proposer, offering); + } + + @DisplayName("게시된 공모에 참여할 수 있다") + @Test + void should_participateSuccess() { + Map request = Map.of( + "memberId", participant.getId().toString(), + "offeringId", offering.getId().toString() + ); + + RestAssured.given(spec).log().all() + .filter(document("participate-success", resource(snippets))) + .contentType(ContentType.JSON) + .body(request) + .when().post("/participations") + .then().log().all() + .statusCode(201); + } + + @DisplayName("공모자는 본인이 만든 공모에 참여할 수 없다") + @Test + void should_throwException_when_givenProposerParticipate() { + Map request = Map.of( + "memberId", proposer.getId().toString(), + "offeringId", offering.getId().toString() + ); + + RestAssured.given(spec).log().all() + .filter(document("participate-fail", resource(snippets))) + .contentType(ContentType.JSON) + .body(request) + .when().post("/participations") + .then().log().all() + .statusCode(500); + } + } +} diff --git a/backend/src/test/resources/application-test.yml b/backend/src/test/resources/application-test.yml new file mode 100644 index 00000000..9f2ba883 --- /dev/null +++ b/backend/src/test/resources/application-test.yml @@ -0,0 +1,13 @@ +spring: + application: + name: chongdae + datasource: + url: jdbc:h2:mem:test + jpa: + show-sql: true + defer-datasource-initialization: true + hibernate: + ddl-auto: create-drop + properties: + hibernate: + format_sql: true