diff --git a/build.gradle b/build.gradle index 8ef9cfdd4..355bb28b0 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' + implementation 'net.logstash.logback:logstash-logback-encoder:8.0' + implementation 'io.jsonwebtoken:jjwt-api:0.12.3' implementation 'org.jsoup:jsoup:1.15.3' @@ -79,6 +81,9 @@ dependencies { implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.1.0' implementation 'org.springframework.boot:spring-boot-starter-aop' + // userAgent parser + implementation 'com.github.ua-parser:uap-java:1.4.4' + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" annotationProcessor "jakarta.annotation:jakarta.annotation-api" diff --git a/src/main/java/in/koreatech/koin/admin/abtest/controller/AbtestApi.java b/src/main/java/in/koreatech/koin/admin/abtest/controller/AbtestApi.java new file mode 100644 index 000000000..b9480ad77 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/controller/AbtestApi.java @@ -0,0 +1,213 @@ +package in.koreatech.koin.admin.abtest.controller; + +import static in.koreatech.koin.domain.user.model.UserType.ADMIN; +import static io.swagger.v3.oas.annotations.enums.ParameterIn.PATH; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import in.koreatech.koin.admin.abtest.dto.AbtestAdminAssignRequest; +import in.koreatech.koin.admin.abtest.dto.AbtestAssignRequest; +import in.koreatech.koin.admin.abtest.dto.AbtestAssignResponse; +import in.koreatech.koin.admin.abtest.dto.AbtestCloseRequest; +import in.koreatech.koin.admin.abtest.dto.AbtestDevicesResponse; +import in.koreatech.koin.admin.abtest.dto.AbtestRequest; +import in.koreatech.koin.admin.abtest.dto.AbtestResponse; +import in.koreatech.koin.admin.abtest.dto.AbtestUsersResponse; +import in.koreatech.koin.admin.abtest.dto.AbtestsResponse; +import in.koreatech.koin.global.auth.Auth; +import in.koreatech.koin.global.auth.UserId; +import in.koreatech.koin.global.useragent.UserAgent; +import in.koreatech.koin.global.useragent.UserAgentInfo; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; + +@RequestMapping("/abtest") +@Tag(name = "(NORMAL, ADMIN) Abtest : AB테스트", description = "AB테스트를 관리한다.") +public interface AbtestApi { + + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "400", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))) + } + ) + @Operation(summary = "(ADMIN) 실험 생성") + @PostMapping + ResponseEntity createAbtest( + @Auth(permit = {ADMIN}) Integer adminId, + @RequestBody @Valid AbtestRequest request + ); + + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "400", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "409", content = @Content(schema = @Schema(hidden = true))) + } + ) + @Operation(summary = "(ADMIN) 실험 수정") + @PutMapping("/{id}") + ResponseEntity putAbtest( + @Auth(permit = {ADMIN}) Integer adminId, + @PathVariable("id") Integer abtestId, + @RequestBody @Valid AbtestRequest request + ); + + @ApiResponses( + value = { + @ApiResponse(responseCode = "204"), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), + } + ) + @Operation(summary = "(ADMIN) 실험 삭제") + @DeleteMapping("/{id}") + ResponseEntity deleteAbtest( + @Auth(permit = {ADMIN}) Integer adminId, + @PathVariable("id") Integer abtestId + ); + + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))) + } + ) + @Operation(summary = "(ADMIN) 실험 목록 조회") + @PostMapping + ResponseEntity getAbtests( + @Auth(permit = {ADMIN}) Integer adminId, + @RequestParam(required = false) Integer page, + @RequestParam(required = false) Integer limit + ); + + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))) + } + ) + @Operation(summary = "(ADMIN) 실험 단건 조회") + @PostMapping("/{id}") + ResponseEntity getAbtest( + @Auth(permit = {ADMIN}) Integer adminId, + @Parameter(in = PATH) @PathVariable("id") Integer articleId + ); + + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), + } + ) + @Operation(summary = "(ADMIN) 실험 종료") + @PostMapping("/close/{id}") + ResponseEntity closeAbtest( + @Auth(permit = {ADMIN}) Integer adminId, + @PathVariable("id") Integer abtestId, + @RequestBody @Valid AbtestCloseRequest abtestCloseRequest + ); + + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), + } + ) + @Operation(summary = "(ADMIN) 이름으로 유저 목록 조회") + @GetMapping("/user") + ResponseEntity getUsersByName( + @Auth(permit = {ADMIN}) Integer adminId, + @RequestParam(value = "name") String userName + ); + + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), + } + ) + @Operation(summary = "(ADMIN) 유저 id로 디바이스 목록 조회") + @GetMapping("/user/{id}/device") + ResponseEntity getDevicesByUserId( + @Auth(permit = {ADMIN}) Integer adminId, + @PathVariable(value = "id") Integer userId + ); + + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "401", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), + } + ) + @Operation(summary = "(ADMIN) 실험군 수동 편입") + @PostMapping("/{id}/move") + ResponseEntity assignAbtestVariableByAdmin( + @Auth(permit = {ADMIN}) Integer adminId, + @PathVariable(value = "id") Integer abtestId, + @RequestBody @Valid AbtestAdminAssignRequest abtestAdminAssignRequest + ); + + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "400", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))) + } + ) + @Operation(summary = "(NORMAL) 자신의 실험군 조회") + @GetMapping("/me") + ResponseEntity getMyAbtestVariable( + @RequestHeader("accessHistoryId") Integer accessHistoryId, + @UserAgent UserAgentInfo userAgentInfo, + @UserId Integer userId, + @RequestParam(name = "title") String title + ); + + @ApiResponses( + value = { + @ApiResponse(responseCode = "200"), + @ApiResponse(responseCode = "400", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))) + } + ) + @Operation(summary = "(NORMAL) 실험군 최초 편입") + @PostMapping("/assign") + ResponseEntity assignAbtestVariable( + @RequestHeader("accessHistoryId") Integer accessHistoryId, + @UserAgent UserAgentInfo userAgentInfo, + @UserId Integer userId, + @RequestBody @Valid AbtestAssignRequest abtestAssignRequest + ); +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/controller/AbtestController.java b/src/main/java/in/koreatech/koin/admin/abtest/controller/AbtestController.java new file mode 100644 index 000000000..13002bbae --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/controller/AbtestController.java @@ -0,0 +1,149 @@ +package in.koreatech.koin.admin.abtest.controller; + +import static in.koreatech.koin.domain.user.model.UserType.ADMIN; +import static io.swagger.v3.oas.annotations.enums.ParameterIn.PATH; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import in.koreatech.koin.admin.abtest.dto.AbtestAdminAssignRequest; +import in.koreatech.koin.admin.abtest.dto.AbtestAssignRequest; +import in.koreatech.koin.admin.abtest.dto.AbtestAssignResponse; +import in.koreatech.koin.admin.abtest.dto.AbtestCloseRequest; +import in.koreatech.koin.admin.abtest.dto.AbtestDevicesResponse; +import in.koreatech.koin.admin.abtest.dto.AbtestRequest; +import in.koreatech.koin.admin.abtest.dto.AbtestResponse; +import in.koreatech.koin.admin.abtest.dto.AbtestUsersResponse; +import in.koreatech.koin.admin.abtest.dto.AbtestsResponse; +import in.koreatech.koin.admin.abtest.service.AbtestService; +import in.koreatech.koin.global.auth.Auth; +import in.koreatech.koin.global.auth.UserId; +import in.koreatech.koin.global.useragent.UserAgent; +import in.koreatech.koin.global.useragent.UserAgentInfo; +import io.swagger.v3.oas.annotations.Parameter; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/abtest") +public class AbtestController implements AbtestApi { + + private final AbtestService abtestService; + + @PostMapping + public ResponseEntity createAbtest( + @Auth(permit = {ADMIN}) Integer adminId, + @RequestBody @Valid AbtestRequest request + ) { + AbtestResponse response = abtestService.createAbtest(request); + return ResponseEntity.ok(response); + } + + @PutMapping("/{id}") + public ResponseEntity putAbtest( + @Auth(permit = {ADMIN}) Integer adminId, + @PathVariable("id") Integer abtestId, + @RequestBody @Valid AbtestRequest request + ) { + AbtestResponse response = abtestService.putAbtest(abtestId, request); + return ResponseEntity.ok(response); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteAbtest( + @Auth(permit = {ADMIN}) Integer adminId, + @PathVariable("id") Integer abtestId + ) { + abtestService.deleteAbtest(abtestId); + return ResponseEntity.noContent().build(); + } + + @GetMapping + public ResponseEntity getAbtests( + @Auth(permit = {ADMIN}) Integer adminId, + @RequestParam(required = false) Integer page, + @RequestParam(required = false) Integer limit + ) { + AbtestsResponse response = abtestService.getAbtests(page, limit); + return ResponseEntity.ok(response); + } + + @GetMapping("/{id}") + public ResponseEntity getAbtest( + @Auth(permit = {ADMIN}) Integer adminId, + @Parameter(in = PATH) @PathVariable("id") Integer abtestId + ) { + AbtestResponse response = abtestService.getAbtest(abtestId); + return ResponseEntity.ok(response); + } + + @PostMapping("/close/{id}") + public ResponseEntity closeAbtest( + @Auth(permit = {ADMIN}) Integer adminId, + @PathVariable("id") Integer abtestId, + @RequestBody @Valid AbtestCloseRequest abtestCloseRequest + ) { + abtestService.closeAbtest(abtestId, abtestCloseRequest); + return ResponseEntity.ok().build(); + } + + @GetMapping("/user") + public ResponseEntity getUsersByName( + @Auth(permit = {ADMIN}) Integer adminId, + @RequestParam(value = "name") String userName + ) { + AbtestUsersResponse response = abtestService.getUsersByName(userName); + return ResponseEntity.ok(response); + } + + @GetMapping("/user/{userId}/device") + public ResponseEntity getDevicesByUserId( + @Auth(permit = {ADMIN}) Integer adminId, + @PathVariable(value = "userId") Integer userId + ) { + AbtestDevicesResponse response = abtestService.getDevicesByUserId(userId); + return ResponseEntity.ok(response); + } + + @PostMapping("/{id}/move") + public ResponseEntity assignAbtestVariableByAdmin( + @Auth(permit = {ADMIN}) Integer adminId, + @PathVariable(value = "id") Integer abtestId, + @RequestBody @Valid AbtestAdminAssignRequest abtestAdminAssignRequest + ) { + abtestService.assignVariableByAdmin(abtestId, abtestAdminAssignRequest); + return ResponseEntity.ok().build(); + } + + @GetMapping("/me") + public ResponseEntity getMyAbtestVariable( + @RequestHeader("access_history_id") Integer accessHistoryId, + @UserAgent UserAgentInfo userAgentInfo, + @UserId Integer userId, + @RequestParam(name = "title") String title + ) { + String response = abtestService.getMyVariable(accessHistoryId, userAgentInfo, userId, title); + return ResponseEntity.ok(response); + } + + @PostMapping("/assign") + public ResponseEntity assignAbtestVariable( + @RequestHeader(value = "access_history_id", required = false) Integer accessHistoryId, + @UserAgent UserAgentInfo userAgentInfo, + @UserId Integer userId, + @RequestBody @Valid AbtestAssignRequest abtestAssignRequest + ) { + AbtestAssignResponse response = abtestService.assignVariable(accessHistoryId, userAgentInfo, userId, abtestAssignRequest); + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/dto/AbtestAdminAssignRequest.java b/src/main/java/in/koreatech/koin/admin/abtest/dto/AbtestAdminAssignRequest.java new file mode 100644 index 000000000..c00ad9617 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/dto/AbtestAdminAssignRequest.java @@ -0,0 +1,19 @@ +package in.koreatech.koin.admin.abtest.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +@JsonNaming(SnakeCaseStrategy.class) +public record AbtestAdminAssignRequest( + @NotNull(message = "디바이스 ID는 필수입니다.") + @Schema(description = "디바이스 ID", example = "1") + Integer deviceId, + + @NotNull(message = "테스트 변수명은 필수입니다.") + @Schema(description = "테스트 변수명", example = "A") + String variableName +) { +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/dto/AbtestAssignRequest.java b/src/main/java/in/koreatech/koin/admin/abtest/dto/AbtestAssignRequest.java new file mode 100644 index 000000000..d218141cd --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/dto/AbtestAssignRequest.java @@ -0,0 +1,17 @@ +package in.koreatech.koin.admin.abtest.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +@JsonNaming(SnakeCaseStrategy.class) +public record AbtestAssignRequest( + @NotNull(message = "테스트 변수명은 필수입니다.") + @Schema(description = "테스트 변수명", example = "dining_ui_test", requiredMode = REQUIRED) + String title +) { +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/dto/AbtestAssignResponse.java b/src/main/java/in/koreatech/koin/admin/abtest/dto/AbtestAssignResponse.java new file mode 100644 index 000000000..99c20ff79 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/dto/AbtestAssignResponse.java @@ -0,0 +1,22 @@ +package in.koreatech.koin.admin.abtest.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import in.koreatech.koin.admin.abtest.model.AbtestVariable; +import in.koreatech.koin.admin.abtest.model.AccessHistory; +import io.swagger.v3.oas.annotations.media.Schema; + +@JsonNaming(SnakeCaseStrategy.class) +public record AbtestAssignResponse ( + @Schema(description = "편입된 변수") + String variableName, + + @Schema(description = "기기 식별자") + Integer accessHistoryId +) { + + public static AbtestAssignResponse of(AbtestVariable abtestVariable, AccessHistory accessHistory) { + return new AbtestAssignResponse(abtestVariable.getName(), accessHistory.getId()); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/dto/AbtestCloseRequest.java b/src/main/java/in/koreatech/koin/admin/abtest/dto/AbtestCloseRequest.java new file mode 100644 index 000000000..acd781631 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/dto/AbtestCloseRequest.java @@ -0,0 +1,15 @@ +package in.koreatech.koin.admin.abtest.dto; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +@JsonNaming(SnakeCaseStrategy.class) +public record AbtestCloseRequest( + @NotNull(message = "승자 이름은 필수입니다.") + @Schema(description = "승자 이름", example = "A") + String winnerName +) { +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/dto/AbtestDevicesResponse.java b/src/main/java/in/koreatech/koin/admin/abtest/dto/AbtestDevicesResponse.java new file mode 100644 index 000000000..8b8918e75 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/dto/AbtestDevicesResponse.java @@ -0,0 +1,47 @@ +package in.koreatech.koin.admin.abtest.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import java.time.LocalDate; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import in.koreatech.koin.admin.abtest.model.Device; +import io.swagger.v3.oas.annotations.media.Schema; + +@JsonNaming(SnakeCaseStrategy.class) +public record AbtestDevicesResponse( + List devices +) { + + public static AbtestDevicesResponse from(List devices) { + return new AbtestDevicesResponse(devices.stream().map(InnerDeviceResponse::from).toList()); + } + + @JsonNaming(SnakeCaseStrategy.class) + private record InnerDeviceResponse( + @Schema(description = "디바이스 ID", example = "1") + Integer id, + + @Schema(description = "디바이스 타입", example = "mobile") + String type, + + @Schema(description = "디바이스 모델", example = "Galaxy20") + String model, + + @Schema(description = "마지막 접속 날짜", example = "2019-07-29", requiredMode = REQUIRED) + @JsonFormat(pattern = "yyyy-MM-dd") LocalDate lastAccessedAt + ) { + + public static InnerDeviceResponse from(Device device) { + return new InnerDeviceResponse( + device.getId(), + device.getType(), + device.getModel(), + device.getAccessHistory().getLastAccessedAt().toLocalDate()); + } + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/dto/AbtestRequest.java b/src/main/java/in/koreatech/koin/admin/abtest/dto/AbtestRequest.java new file mode 100644 index 000000000..1f5516b7e --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/dto/AbtestRequest.java @@ -0,0 +1,52 @@ +package in.koreatech.koin.admin.abtest.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import java.util.List; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +@JsonNaming(SnakeCaseStrategy.class) +public record AbtestRequest( + @NotBlank(message = "실험명은 필수입니다.") + @Schema(description = "실험명", example = "식단 UI 실험", requiredMode = REQUIRED) + String displayTitle, + + @Schema(description = "실험 생성자 이름", example = "홍길동", requiredMode = REQUIRED) + String creator, + + @Schema(description = "팀명", example = "campus", requiredMode = REQUIRED) + String team, + + @NotBlank(message = "실험명(변수명)은 필수입니다.") + @Schema(description = "실험명(변수명)", example = "dining_ui_test", requiredMode = REQUIRED) + String title, + + @Schema(description = "실험 내용", example = "식단 UI 변경에 따른 사용자 변화량 조사", requiredMode = REQUIRED) + String description, + + List variables +) { + + @JsonNaming(SnakeCaseStrategy.class) + public record InnerVariableRequest( + + @NotNull(message = "실험군 편입 비율은 필수입니다.") + @Schema(description = "실험군 편입 비율", example = "33", requiredMode = REQUIRED) + Integer rate, + + @NotBlank(message = "실험군 이름은 필수입니다.") + @Schema(description = "실험군 이름", example = "실험군 A", requiredMode = REQUIRED) + String displayName, + + @NotBlank(message = "실험군 이름(변수명)은 필수입니다.") + @Schema(description = "실험군 이름(변수명)", example = "A", requiredMode = REQUIRED) + String name + ) { + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/dto/AbtestResponse.java b/src/main/java/in/koreatech/koin/admin/abtest/dto/AbtestResponse.java new file mode 100644 index 000000000..07229e5c6 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/dto/AbtestResponse.java @@ -0,0 +1,89 @@ +package in.koreatech.koin.admin.abtest.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import java.time.LocalDateTime; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import in.koreatech.koin.admin.abtest.model.Abtest; +import in.koreatech.koin.admin.abtest.model.AbtestVariable; +import io.swagger.v3.oas.annotations.media.Schema; + +@JsonNaming(SnakeCaseStrategy.class) +public record AbtestResponse( + @Schema(description = "실험 ID", example = "1", requiredMode = REQUIRED) + Integer id, + + @Schema(description = "실험명", example = "식단 UI 실험", requiredMode = REQUIRED) + String displayTitle, + + @Schema(description = "실험 생성자 이름", example = "홍길동", requiredMode = REQUIRED) + String creator, + + @Schema(description = "팀명", example = "campus", requiredMode = REQUIRED) + String team, + + @Schema(description = "실험 생성자 이름", example = "홍길동", requiredMode = REQUIRED) + String status, + + @Schema(description = "실험 생성자 이름", example = "홍길동", requiredMode = REQUIRED) + String winnerName, + + @Schema(description = "실험명(변수명)", example = "dining_ui_test", requiredMode = REQUIRED) + String title, + + @Schema(description = "실험 내용", example = "식단 UI 변경에 따른 사용자 변화량 조사", requiredMode = NOT_REQUIRED) + String description, + + List variables, + + @Schema(description = "생성 일자", example = "2023-01-04 12:00:01") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime createdAt, + + @Schema(description = "수정 일자", example = "2023-01-04 12:00:01") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime updatedAt +) { + + public static AbtestResponse from(Abtest abtest) { + return new AbtestResponse( + abtest.getId(), + abtest.getDisplayTitle(), + abtest.getCreator(), + abtest.getTeam(), + abtest.getStatus().name(), + abtest.getWinnerName(), + abtest.getTitle(), + abtest.getDescription(), + abtest.getAbtestVariables().stream() + .map(InnerVariableResponse::from) + .toList(), + abtest.getCreatedAt(), + abtest.getUpdatedAt() + ); + } + + @JsonNaming(SnakeCaseStrategy.class) + private record InnerVariableResponse( + @Schema(description = "실험군 편입 비율", example = "33", requiredMode = REQUIRED) + Integer rate, + + @Schema(description = "실험군 이름", example = "실험군 A", requiredMode = REQUIRED) + String displayName, + + @Schema(description = "실험군 이름(변수명)", example = "A", requiredMode = REQUIRED) + String name + ) { + public static InnerVariableResponse from(AbtestVariable abtestVariable) { + return new InnerVariableResponse( + abtestVariable.getRate(), + abtestVariable.getDisplayName(), + abtestVariable.getName() + ); + } + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/dto/AbtestUsersResponse.java b/src/main/java/in/koreatech/koin/admin/abtest/dto/AbtestUsersResponse.java new file mode 100644 index 000000000..9eb5732ec --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/dto/AbtestUsersResponse.java @@ -0,0 +1,40 @@ +package in.koreatech.koin.admin.abtest.dto; + +import java.util.List; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import in.koreatech.koin.domain.user.model.User; +import in.koreatech.koin.domain.user.model.UserType; +import io.swagger.v3.oas.annotations.media.Schema; + +@JsonNaming(SnakeCaseStrategy.class) +public record AbtestUsersResponse( + List users +) { + + public static AbtestUsersResponse from(List users) { + return new AbtestUsersResponse(users.stream().map(InnerUserResponse::from).toList()); + } + + @JsonNaming(SnakeCaseStrategy.class) + private record InnerUserResponse( + @Schema(description = "사용자 ID", example = "1") + Integer id, + + @Schema(description = "사용자 이름", example = "홍길동") + String name, + + @Schema(description = "사용자 상세 정보(전화번호 or 이메일)", example = "010-1234-5678") + String detail + ) { + + public static InnerUserResponse from(User user) { + if (user.getUserType().equals(UserType.OWNER)) { + return new InnerUserResponse(user.getId(), user.getName(), user.getPhoneNumber()); + } + return new InnerUserResponse(user.getId(), user.getName(), user.getEmail()); + } + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/dto/AbtestsResponse.java b/src/main/java/in/koreatech/koin/admin/abtest/dto/AbtestsResponse.java new file mode 100644 index 000000000..3f9142901 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/dto/AbtestsResponse.java @@ -0,0 +1,92 @@ +package in.koreatech.koin.admin.abtest.dto; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +import java.time.LocalDateTime; +import java.util.List; + +import org.springframework.data.domain.Page; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +import in.koreatech.koin.admin.abtest.model.Abtest; +import in.koreatech.koin.global.model.Criteria; +import io.swagger.v3.oas.annotations.media.Schema; + +@JsonNaming(SnakeCaseStrategy.class) +public record AbtestsResponse( + @Schema(description = "AB테스트 목록", required = true) + List tests, + + @Schema(example = "85", description = "전체 실험 수", required = true) + Long totalCount, + + @Schema(example = "10", description = "현재 페이지 실험 수", required = true) + Integer currentCount, + + @Schema(example = "9", description = "전체 페이지 수", required = true) + Integer totalPage, + + @Schema(example = "1", description = "현재 페이지", required = true) + Integer currentPage +) { + + public static AbtestsResponse of(Page pagedResult, Criteria criteria) { + return new AbtestsResponse( + pagedResult.stream() + .map(InnerAbtestResponse::from) + .toList(), + pagedResult.getTotalElements(), + pagedResult.getContent().size(), + pagedResult.getTotalPages(), + criteria.getPage() + 1 + ); + } + + @JsonNaming(SnakeCaseStrategy.class) + private record InnerAbtestResponse( + + @Schema(example = "1", description = "AB테스트 고유 id", required = true) + Integer id, + + @Schema(example = "IN_PROGRESS", description = "AB테스트 상태", required = true) + String status, + + @Schema(example = "A", description = "승자 실험군 이름", required = true) + String winnerName, + + @Schema(example = "송선권", description = "AB테스트 생성자", required = true) + String creator, + + @Schema(example = "campus", description = "AB테스트 팀명", required = true) + String team, + + @Schema(example = "식단 테스트", description = "AB테스트 제목", required = true) + String displayTitle, + + @Schema(example = "dining_ui_test", description = "AB테스트 제목(변수명)", required = true) + String title, + + @Schema(description = "생성 일자", example = "2023-01-04 12:00:01", requiredMode = REQUIRED) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime createdAt, + + @Schema(description = "수정 일자", example = "2023-01-04 12:00:01", requiredMode = REQUIRED) + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime updatedAt + ) { + public static InnerAbtestResponse from(Abtest abtest) { + return new InnerAbtestResponse( + abtest.getId(), + abtest.getStatus().name(), + abtest.getWinnerName(), + abtest.getCreator(), + abtest.getTeam(), + abtest.getDisplayTitle(), + abtest.getTitle(), + abtest.getCreatedAt(), + abtest.getUpdatedAt() + ); + } + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestAlreadyExistException.java b/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestAlreadyExistException.java new file mode 100644 index 000000000..5622b85bb --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestAlreadyExistException.java @@ -0,0 +1,20 @@ +package in.koreatech.koin.admin.abtest.exception; + +import in.koreatech.koin.global.exception.DuplicationException; + +public class AbtestAlreadyExistException extends DuplicationException { + + private static final String DEFAULT_MESSAGE = "이미 존재하는 AB테스트입니다."; + + public AbtestAlreadyExistException(String message) { + super(message); + } + + public AbtestAlreadyExistException(String message, String detail) { + super(message, detail); + } + + public static AbtestAlreadyExistException withDetail(String detail) { + return new AbtestAlreadyExistException(DEFAULT_MESSAGE, detail); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestAssignException.java b/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestAssignException.java new file mode 100644 index 000000000..d90164ecb --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestAssignException.java @@ -0,0 +1,20 @@ +package in.koreatech.koin.admin.abtest.exception; + +import in.koreatech.koin.global.exception.KoinIllegalStateException; + +public class AbtestAssignException extends KoinIllegalStateException { + + private static final String DEFAULT_MESSAGE = "AB테스트 편입 중 오류가 발생했습니다."; + + public AbtestAssignException(String message) { + super(message); + } + + public AbtestAssignException(String message, String detail) { + super(message, detail); + } + + public static AbtestAssignException withDetail(String detail) { + return new AbtestAssignException(DEFAULT_MESSAGE, detail); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestAssignedUserException.java b/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestAssignedUserException.java new file mode 100644 index 000000000..39ffa08c7 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestAssignedUserException.java @@ -0,0 +1,20 @@ +package in.koreatech.koin.admin.abtest.exception; + +import in.koreatech.koin.global.exception.KoinIllegalArgumentException; + +public class AbtestAssignedUserException extends KoinIllegalArgumentException { + + private static final String DEFAULT_MESSAGE = "이미 편입된 사용자입니다."; + + public AbtestAssignedUserException(String message) { + super(message); + } + + public AbtestAssignedUserException(String message, String detail) { + super(message, detail); + } + + public static AbtestAssignedUserException withDetail(String detail) { + return new AbtestAssignedUserException(DEFAULT_MESSAGE, detail); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestDuplicatedVariableException.java b/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestDuplicatedVariableException.java new file mode 100644 index 000000000..070ee4387 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestDuplicatedVariableException.java @@ -0,0 +1,20 @@ +package in.koreatech.koin.admin.abtest.exception; + +import in.koreatech.koin.global.exception.DuplicationException; + +public class AbtestDuplicatedVariableException extends DuplicationException { + + private static final String DEFAULT_MESSAGE = "실험군이 중복됩니다."; + + public AbtestDuplicatedVariableException(String message) { + super(message); + } + + public AbtestDuplicatedVariableException(String message, String detail) { + super(message, detail); + } + + public static AbtestDuplicatedVariableException withDetail(String detail) { + return new AbtestDuplicatedVariableException(DEFAULT_MESSAGE, detail); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestNotAssignedUserException.java b/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestNotAssignedUserException.java new file mode 100644 index 000000000..42bbabfc7 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestNotAssignedUserException.java @@ -0,0 +1,20 @@ +package in.koreatech.koin.admin.abtest.exception; + +import in.koreatech.koin.global.exception.KoinIllegalArgumentException; + +public class AbtestNotAssignedUserException extends KoinIllegalArgumentException { + + private static final String DEFAULT_MESSAGE = "편입되지 않은 사용자입니다."; + + public AbtestNotAssignedUserException(String message) { + super(message); + } + + public AbtestNotAssignedUserException(String message, String detail) { + super(message, detail); + } + + public static AbtestNotAssignedUserException withDetail(String detail) { + return new AbtestNotAssignedUserException(DEFAULT_MESSAGE, detail); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestNotFoundException.java b/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestNotFoundException.java new file mode 100644 index 000000000..7fd7053e4 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestNotFoundException.java @@ -0,0 +1,20 @@ +package in.koreatech.koin.admin.abtest.exception; + +import in.koreatech.koin.global.exception.DataNotFoundException; + +public class AbtestNotFoundException extends DataNotFoundException { + + private static final String DEFAULT_MESSAGE = "존재하지 않는 Abtest입니다."; + + public AbtestNotFoundException(String message) { + super(message); + } + + public AbtestNotFoundException(String message, String detail) { + super(message, detail); + } + + public static AbtestNotFoundException withDetail(String detail) { + return new AbtestNotFoundException(DEFAULT_MESSAGE, detail); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestNotInProgressException.java b/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestNotInProgressException.java new file mode 100644 index 000000000..e86c84cad --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestNotInProgressException.java @@ -0,0 +1,20 @@ +package in.koreatech.koin.admin.abtest.exception; + +import in.koreatech.koin.global.exception.KoinIllegalArgumentException; + +public class AbtestNotInProgressException extends KoinIllegalArgumentException { + + private static final String DEFAULT_MESSAGE = "진행중이지 않은 AB테스트입니다."; + + public AbtestNotInProgressException(String message) { + super(message); + } + + public AbtestNotInProgressException(String message, String detail) { + super(message, detail); + } + + public static AbtestNotInProgressException withDetail(String detail) { + return new AbtestNotInProgressException(DEFAULT_MESSAGE, detail); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestNotIncludeVariableException.java b/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestNotIncludeVariableException.java new file mode 100644 index 000000000..1c884e410 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestNotIncludeVariableException.java @@ -0,0 +1,20 @@ +package in.koreatech.koin.admin.abtest.exception; + +import in.koreatech.koin.global.exception.KoinIllegalArgumentException; + +public class AbtestNotIncludeVariableException extends KoinIllegalArgumentException { + + private static final String DEFAULT_MESSAGE = "요청된 AB테스트 내에 실험군이 존재하지 않습니다."; + + public AbtestNotIncludeVariableException(String message) { + super(message); + } + + public AbtestNotIncludeVariableException(String message, String detail) { + super(message, detail); + } + + public static AbtestNotIncludeVariableException withDetail(String detail) { + return new AbtestNotIncludeVariableException(DEFAULT_MESSAGE, detail); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestTitleIllegalArgumentException.java b/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestTitleIllegalArgumentException.java new file mode 100644 index 000000000..02fee92ab --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestTitleIllegalArgumentException.java @@ -0,0 +1,20 @@ +package in.koreatech.koin.admin.abtest.exception; + +import in.koreatech.koin.global.exception.KoinIllegalArgumentException; + +public class AbtestTitleIllegalArgumentException extends KoinIllegalArgumentException { + + private static final String DEFAULT_MESSAGE = "실험 이름이 잘못되었습니다."; + + public AbtestTitleIllegalArgumentException(String message) { + super(message); + } + + public AbtestTitleIllegalArgumentException(String message, String detail) { + super(message, detail); + } + + public static AbtestTitleIllegalArgumentException withDetail(String detail) { + return new AbtestTitleIllegalArgumentException(DEFAULT_MESSAGE, detail); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestVariableCountNotFoundException.java b/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestVariableCountNotFoundException.java new file mode 100644 index 000000000..d745f9b3d --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestVariableCountNotFoundException.java @@ -0,0 +1,20 @@ +package in.koreatech.koin.admin.abtest.exception; + +import in.koreatech.koin.global.exception.DataNotFoundException; + +public class AbtestVariableCountNotFoundException extends DataNotFoundException { + + private static final String DEFAULT_MESSAGE = "존재하지 않는 AbtestVariableCount입니다."; + + public AbtestVariableCountNotFoundException(String message) { + super(message); + } + + public AbtestVariableCountNotFoundException(String message, String detail) { + super(message, detail); + } + + public static AbtestVariableCountNotFoundException withDetail(String detail) { + return new AbtestVariableCountNotFoundException(DEFAULT_MESSAGE, detail); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestVariableIllegalArgumentException.java b/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestVariableIllegalArgumentException.java new file mode 100644 index 000000000..6b2eb8d65 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestVariableIllegalArgumentException.java @@ -0,0 +1,20 @@ +package in.koreatech.koin.admin.abtest.exception; + +import in.koreatech.koin.global.exception.KoinIllegalArgumentException; + +public class AbtestVariableIllegalArgumentException extends KoinIllegalArgumentException { + + private static final String DEFAULT_MESSAGE = "실험군이 잘못되었습니다."; + + public AbtestVariableIllegalArgumentException(String message) { + super(message); + } + + public AbtestVariableIllegalArgumentException(String message, String detail) { + super(message, detail); + } + + public static AbtestVariableIllegalArgumentException withDetail(String detail) { + return new AbtestVariableIllegalArgumentException(DEFAULT_MESSAGE, detail); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestVariableNotFoundException.java b/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestVariableNotFoundException.java new file mode 100644 index 000000000..c2544dbac --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestVariableNotFoundException.java @@ -0,0 +1,20 @@ +package in.koreatech.koin.admin.abtest.exception; + +import in.koreatech.koin.global.exception.DataNotFoundException; + +public class AbtestVariableNotFoundException extends DataNotFoundException { + + private static final String DEFAULT_MESSAGE = "존재하지 않는 AbtestVariable입니다."; + + public AbtestVariableNotFoundException(String message) { + super(message); + } + + public AbtestVariableNotFoundException(String message, String detail) { + super(message, detail); + } + + public static AbtestVariableNotFoundException withDetail(String detail) { + return new AbtestVariableNotFoundException(DEFAULT_MESSAGE, detail); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestWinnerNotDecidedException.java b/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestWinnerNotDecidedException.java new file mode 100644 index 000000000..417097a38 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/exception/AbtestWinnerNotDecidedException.java @@ -0,0 +1,20 @@ +package in.koreatech.koin.admin.abtest.exception; + +import in.koreatech.koin.global.exception.KoinIllegalStateException; + +public class AbtestWinnerNotDecidedException extends KoinIllegalStateException { + + private static final String DEFAULT_MESSAGE = "종료되었으나 우승자가 결정되지 않은 AB테스트입니다."; + + public AbtestWinnerNotDecidedException(String message) { + super(message); + } + + public AbtestWinnerNotDecidedException(String message, String detail) { + super(message, detail); + } + + public static AbtestWinnerNotDecidedException withDetail(String detail) { + return new AbtestWinnerNotDecidedException(DEFAULT_MESSAGE, detail); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/exception/AccessHistoryNotFoundException.java b/src/main/java/in/koreatech/koin/admin/abtest/exception/AccessHistoryNotFoundException.java new file mode 100644 index 000000000..58fb14946 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/exception/AccessHistoryNotFoundException.java @@ -0,0 +1,20 @@ +package in.koreatech.koin.admin.abtest.exception; + +import in.koreatech.koin.global.exception.DataNotFoundException; + +public class AccessHistoryNotFoundException extends DataNotFoundException { + + private static final String DEFAULT_MESSAGE = "존재하지 않는 접속 이력입니다."; + + public AccessHistoryNotFoundException(String message) { + super(message); + } + + public AccessHistoryNotFoundException(String message, String detail) { + super(message, detail); + } + + public static AccessHistoryNotFoundException withDetail(String detail) { + return new AccessHistoryNotFoundException(DEFAULT_MESSAGE, detail); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/exception/DeviceNotFoundException.java b/src/main/java/in/koreatech/koin/admin/abtest/exception/DeviceNotFoundException.java new file mode 100644 index 000000000..824168026 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/exception/DeviceNotFoundException.java @@ -0,0 +1,20 @@ +package in.koreatech.koin.admin.abtest.exception; + +import in.koreatech.koin.global.exception.DataNotFoundException; + +public class DeviceNotFoundException extends DataNotFoundException { + + private static final String DEFAULT_MESSAGE = "존재하지 않는 디바이스입니다."; + + public DeviceNotFoundException(String message) { + super(message); + } + + public DeviceNotFoundException(String message, String detail) { + super(message, detail); + } + + public static DeviceNotFoundException withDetail(String detail) { + return new DeviceNotFoundException(DEFAULT_MESSAGE, detail); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/model/Abtest.java b/src/main/java/in/koreatech/koin/admin/abtest/model/Abtest.java new file mode 100644 index 000000000..a3d1e96ad --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/model/Abtest.java @@ -0,0 +1,241 @@ +package in.koreatech.koin.admin.abtest.model; + +import static lombok.AccessLevel.PROTECTED; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import in.koreatech.koin.admin.abtest.dto.AbtestRequest; +import in.koreatech.koin.admin.abtest.exception.AbtestAssignException; +import in.koreatech.koin.admin.abtest.exception.AbtestNotIncludeVariableException; +import in.koreatech.koin.admin.abtest.exception.AbtestTitleIllegalArgumentException; +import in.koreatech.koin.admin.abtest.exception.AbtestVariableIllegalArgumentException; +import in.koreatech.koin.admin.abtest.model.redis.AbtestVariableCount; +import in.koreatech.koin.global.domain.BaseEntity; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@NoArgsConstructor(access = PROTECTED) +@Table(name = "abtest", schema = "koin") +public class Abtest extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Integer id; + + @Size(max = 255) + @NotNull + @Column(name = "title", nullable = false, unique = true) + private String title; + + @Size(max = 255) + @NotNull + @Column(name = "display_title", nullable = false) + private String displayTitle; + + @Size(max = 255) + @Column(name = "description") + private String description; + + @Size(max = 50) + @Column(name = "creator") + private String creator; + + @Size(max = 50) + @Column(name = "team") + private String team; + + @NotNull + @Column(name = "status", nullable = false) + @Enumerated(EnumType.STRING) + private AbtestStatus status; + + @OneToMany(mappedBy = "abtest", cascade = CascadeType.ALL, orphanRemoval = true) + private List abtestVariables = new ArrayList<>(); + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "winner_id") + private AbtestVariable winner; + + @Builder + private Abtest( + Integer id, + String title, + String displayTitle, + String description, + String creator, + String team, + AbtestStatus status + ) { + this.id = id; + this.title = title; + this.displayTitle = displayTitle; + this.description = description; + this.creator = creator; + this.team = team; + this.status = status; + } + + public AbtestVariable findAssignVariable(List cacheCounts) { + // dbCount와 cacheCount를 병합하여 현재 카운트를 계산 + Map currentCounts = abtestVariables.stream() + .collect(Collectors.toMap(AbtestVariable::getId, AbtestVariable::getCount)); + + cacheCounts.forEach(count -> currentCounts.merge( + count.getVariableId(), + count.getCount(), + Integer::sum + )); + + // 총 레코드 수 계산 + int totalCount = currentCounts.values().stream().mapToInt(Integer::intValue).sum(); + if (totalCount == 0) { + return abtestVariables.get(0); + } + + // 각 변수의 차이 계산하여 가장 큰 차이를 갖는 변수 선택 + int targetVariable = abtestVariables.stream() + .map(variable -> { + int currentRate = currentCounts.get(variable.getId()) * 100 / totalCount; + int difference = variable.getRate() - currentRate; + return new AbstractMap.SimpleEntry<>(variable.getId(), difference); + }) + .max(Map.Entry.comparingByValue()) + .orElseThrow(() -> AbtestAssignException.withDetail("abtest name: " + title)) + .getKey(); + + // 타겟 변수 반환 + return abtestVariables.stream() + .filter(variable -> variable.getId() == targetVariable) + .findAny() + .orElseThrow(() -> AbtestAssignException.withDetail("abtest name: " + title)); + } + + public void setVariables(List variables, EntityManager entityManager) { + vaildateVariables(variables); + List saved = variables.stream() + .map(request -> + AbtestVariable.builder() + .abtest(this) + .displayName(request.displayName()) + .rate(request.rate()) + .name(request.name()) + .build() + ).toList(); + abtestVariables.clear(); + entityManager.flush(); + abtestVariables.addAll(saved); + } + + public void update(String displayTitle, String creator, String team, String title, String description, + List variables) { + if (!this.title.equals(title)) { + throw AbtestTitleIllegalArgumentException.withDetail("실험 title은 변경할 수 없습니다."); + } + vaildateVariables(variables); + validatePutVariables(this, variables); + updateVariables(variables); + this.displayTitle = displayTitle; + this.creator = creator; + this.team = team; + this.description = description; + } + + private static void vaildateVariables(List variables) { + int sum = variables.stream().mapToInt(AbtestRequest.InnerVariableRequest::rate).sum(); + if (sum != 100) { + throw AbtestVariableIllegalArgumentException.withDetail("실험군 비율 합이 100이 아닙니다. rate sum: " + sum); + } + + int distinctSize = variables.stream() + .map(AbtestRequest.InnerVariableRequest::name) + .distinct().toList().size(); + if (distinctSize != variables.size()) { + throw AbtestVariableIllegalArgumentException.withDetail("실험군 간의 변수명(name)이 중복됩니다."); + } + } + + private void updateVariables(List requestVariables) { + requestVariables.forEach(requestVariable -> { + AbtestVariable variable = abtestVariables.stream() + .filter(abtestVariable -> abtestVariable.getName().equals(requestVariable.name())) + .findAny() + .orElseThrow(() -> AbtestVariableIllegalArgumentException.withDetail( + "abtest name: " + title + ", variable name: " + requestVariable.name())); + variable.update(requestVariable.displayName(), requestVariable.rate()); + }); + } + + private void validatePutVariables(Abtest abtest, List variables) { + if (abtest.getAbtestVariables().size() != variables.size()) { + throw AbtestVariableIllegalArgumentException.withDetail("실험군 개수는 수정될 수 없습니다."); + } + } + + public String getWinnerName() { + return winner != null ? winner.getName() : null; + } + + public void close(String winnerName) { + status = AbtestStatus.CLOSED; + if (winnerName != null) { + winner = getVariableByName(winnerName); + } + abtestVariables.forEach(AbtestVariable::close); + } + + public void assignVariableByAdmin(AccessHistory accessHistory, String variableName) { + resetExistVariable(accessHistory); + AbtestVariable variable = getVariableByName(variableName); + accessHistory.addVariable(variable); + variable.addCount(1); + } + + public AbtestVariable getVariableByName(String variableName) { + return abtestVariables.stream() + .filter(abtestVariable -> abtestVariable.getName().equals(variableName)) + .findAny() + .orElseThrow(() -> AbtestNotIncludeVariableException.withDetail( + "abtest name: " + title + ", winner name: " + variableName)); + } + + private void resetExistVariable(AccessHistory accessHistory) { + Optional variable = findVariableByAccessHistory(accessHistory); + if (variable.isEmpty()) { + return; + } + variable.get().addCount(-1); + accessHistory.removeVariable(variable.get()); + } + + public Optional findVariableByAccessHistory(AccessHistory accessHistory) { + return accessHistory.getAccessHistoryAbtestVariables().stream() + .filter(map -> map.getAccessHistory().getId().equals(accessHistory.getId())) + .map(AccessHistoryAbtestVariable::getVariable) + .findAny(); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/model/AbtestStatus.java b/src/main/java/in/koreatech/koin/admin/abtest/model/AbtestStatus.java new file mode 100644 index 000000000..4f1d31da8 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/model/AbtestStatus.java @@ -0,0 +1,10 @@ +package in.koreatech.koin.admin.abtest.model; + +import lombok.Getter; + +@Getter +public enum AbtestStatus { + IN_PROGRESS, + CLOSED, + ; +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/model/AbtestVariable.java b/src/main/java/in/koreatech/koin/admin/abtest/model/AbtestVariable.java new file mode 100644 index 000000000..18f070f19 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/model/AbtestVariable.java @@ -0,0 +1,90 @@ +package in.koreatech.koin.admin.abtest.model; + +import static lombok.AccessLevel.PROTECTED; + +import java.util.ArrayList; +import java.util.List; + +import in.koreatech.koin.global.domain.BaseEntity; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@NoArgsConstructor(access = PROTECTED) +@Table(name = "abtest_variable", schema = "koin") +public class AbtestVariable extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Integer id; + + @NotNull + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "abtest_id", nullable = false) + private Abtest abtest; + + @Size(max = 255) + @NotNull + @Column(name = "name", nullable = false) + private String name; + + @Size(max = 255) + @NotNull + @Column(name = "display_name", nullable = false) + private String displayName; + + @Column(name = "rate", nullable = false) + private Integer rate; + + @Column(name = "count", nullable = false) + private Integer count = 0; + + @OneToMany(mappedBy = "variable", orphanRemoval = true, cascade = CascadeType.ALL) + private List accessHistoryAbtestVariables = new ArrayList<>(); + + @Builder + private AbtestVariable( + Integer id, + Abtest abtest, + String name, + String displayName, + Integer rate, + Integer count + ) { + this.id = id; + this.abtest = abtest; + this.name = name; + this.displayName = displayName; + this.rate = rate; + this.count = count != null ? count : 0; + } + + public void addCount(int count) { + this.count += count; + } + + public void update(String displayName, Integer rate) { + this.displayName = displayName; + this.rate = rate; + } + + public void close() { + accessHistoryAbtestVariables.clear(); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/model/AccessHistory.java b/src/main/java/in/koreatech/koin/admin/abtest/model/AccessHistory.java new file mode 100644 index 000000000..4f5fb38d1 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/model/AccessHistory.java @@ -0,0 +1,117 @@ +package in.koreatech.koin.admin.abtest.model; + +import static lombok.AccessLevel.PROTECTED; + +import java.time.Clock; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import org.springframework.data.annotation.CreatedDate; + +import in.koreatech.koin.global.domain.BaseEntity; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@NoArgsConstructor(access = PROTECTED) +@Table(name = "access_history", schema = "koin") +public class AccessHistory extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Integer id; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "device_id") + private Device device; + + @OneToMany(mappedBy = "accessHistory", orphanRemoval = true, cascade = CascadeType.ALL) + private List accessHistoryAbtestVariables = new ArrayList<>(); + + @NotNull + @Column(name = "last_accessed_at", nullable = false, columnDefinition = "TIMESTAMP") + @CreatedDate + private LocalDateTime lastAccessedAt; + + public Optional findVariableByAbtestId(int abtestId) { + return accessHistoryAbtestVariables.stream() + .map(AccessHistoryAbtestVariable::getVariable) + .filter(abtestVariable -> abtestVariable.getAbtest().getId().equals(abtestId)) + .findAny(); + } + + @Builder + private AccessHistory( + Integer id, + Device device, + LocalDateTime lastAccessedAt + ) { + this.id = id; + this.device = device; + this.lastAccessedAt = lastAccessedAt; + } + + public void connectDevice(Device device) { + this.device = device; + device.setAccessHistory(this); + } + + public void addVariable(AbtestVariable variable) { + accessHistoryAbtestVariables.add(AccessHistoryAbtestVariable.builder() + .accessHistory(this) + .variable(variable) + .build()); + } + + public List getVariableBy(Abtest abtest) { + return accessHistoryAbtestVariables.stream() + .map(AccessHistoryAbtestVariable::getVariable) + .filter(abtestVariable -> abtestVariable.getAbtest().equals(abtest)) + .toList(); + } + + public void addAbtestVariable(AbtestVariable variable) { + accessHistoryAbtestVariables.removeIf(map -> map.getVariable().getAbtest().equals(variable.getAbtest())); + + AccessHistoryAbtestVariable saved = AccessHistoryAbtestVariable.builder() + .accessHistory(this) + .variable(variable) + .build(); + accessHistoryAbtestVariables.add(saved); + variable.getAccessHistoryAbtestVariables().add(saved); + } + + public boolean hasVariable(Integer variableId) { + return accessHistoryAbtestVariables.stream() + .map(AccessHistoryAbtestVariable::getVariable) + .anyMatch(abtestVariable -> Objects.equals(abtestVariable.getId(), variableId)); + } + + public void removeVariable(AbtestVariable variable) { + accessHistoryAbtestVariables.removeIf( + accessHistoryAbtestVariable -> accessHistoryAbtestVariable.getVariable().equals(variable) + ); + } + + public void updateLastAccessedAt(Clock clock) { + this.lastAccessedAt = LocalDateTime.now(clock); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/model/AccessHistoryAbtestVariable.java b/src/main/java/in/koreatech/koin/admin/abtest/model/AccessHistoryAbtestVariable.java new file mode 100644 index 000000000..b00e2193a --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/model/AccessHistoryAbtestVariable.java @@ -0,0 +1,51 @@ +package in.koreatech.koin.admin.abtest.model; + +import static lombok.AccessLevel.PROTECTED; + +import in.koreatech.koin.global.domain.BaseEntity; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@NoArgsConstructor(access = PROTECTED) +@Table(name = "access_history_abtest_variable", schema = "koin") +public class AccessHistoryAbtestVariable extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Integer id; + + @NotNull + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "access_history_id", nullable = false) + private AccessHistory accessHistory; + + @NotNull + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "variable_id", nullable = false) + private AbtestVariable variable; + + @Builder + private AccessHistoryAbtestVariable( + Integer id, + AccessHistory accessHistory, + AbtestVariable variable + ) { + this.id = id; + this.accessHistory = accessHistory; + this.variable = variable; + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/model/Device.java b/src/main/java/in/koreatech/koin/admin/abtest/model/Device.java new file mode 100644 index 000000000..cdd678cc6 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/model/Device.java @@ -0,0 +1,78 @@ +package in.koreatech.koin.admin.abtest.model; + +import static lombok.AccessLevel.PROTECTED; + +import in.koreatech.koin.domain.user.model.User; +import in.koreatech.koin.global.domain.BaseEntity; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Entity +@NoArgsConstructor(access = PROTECTED) +@Table(name = "device", schema = "koin") +public class Device extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Integer id; + + @OneToOne(mappedBy = "device", cascade = CascadeType.PERSIST) + private AccessHistory accessHistory; + + @NotNull + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Size(max = 100) + @Column(name = "model", length = 100) + private String model; + + @Size(max = 100) + @Column(name = "type", length = 100) + private String type; + + @Builder + private Device( + Integer id, + AccessHistory accessHistory, + User user, + String model, + String type + ) { + this.id = id; + this.accessHistory = accessHistory; + this.user = user; + this.model = model; + this.type = type; + } + + public void setAccessHistory(AccessHistory accessHistory) { + this.accessHistory = accessHistory; + } + + public void changeUser(User user) { + this.user = user; + } + + public void setModelInfo(String model, String type) { + this.model = model; + this.type = type; + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/model/redis/AbtestVariableAssign.java b/src/main/java/in/koreatech/koin/admin/abtest/model/redis/AbtestVariableAssign.java new file mode 100644 index 000000000..d906b8a36 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/model/redis/AbtestVariableAssign.java @@ -0,0 +1,38 @@ +package in.koreatech.koin.admin.abtest.model.redis; + +import java.util.concurrent.TimeUnit; + +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.TimeToLive; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@RedisHash("AbtestVariableAssign") +public class AbtestVariableAssign { + + public static final String DELIMITER = ":"; + + private static final long CACHE_EXPIRE_DAYS = 3L; + + @Id + private String id; + + @TimeToLive(unit = TimeUnit.DAYS) + private final Long expiration; + + @Builder + private AbtestVariableAssign(String id, Long expiration) { + this.id = id; + this.expiration = expiration; + } + + public static AbtestVariableAssign of(Integer variableId, Integer accessHistoryId) { + return AbtestVariableAssign.builder() + .id(variableId + DELIMITER + accessHistoryId) + .expiration(CACHE_EXPIRE_DAYS) + .build(); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/model/redis/AbtestVariableCount.java b/src/main/java/in/koreatech/koin/admin/abtest/model/redis/AbtestVariableCount.java new file mode 100644 index 000000000..c3b8c6698 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/model/redis/AbtestVariableCount.java @@ -0,0 +1,34 @@ +package in.koreatech.koin.admin.abtest.model.redis; + +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@RedisHash("AbtestVariableCount") +public class AbtestVariableCount { + + @Id + private Integer variableId; + private Integer count; + + @Builder + private AbtestVariableCount(Integer variableId, Integer count) { + this.variableId = variableId; + this.count = count; + } + + public void resetCount() { + count = 0; + } + + public void addCount() { + count += 1; + } + + public void minusCount() { + count -= 1; + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/repository/AbtestRepository.java b/src/main/java/in/koreatech/koin/admin/abtest/repository/AbtestRepository.java new file mode 100644 index 000000000..aea95c558 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/repository/AbtestRepository.java @@ -0,0 +1,34 @@ +package in.koreatech.koin.admin.abtest.repository; + +import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.Repository; + +import in.koreatech.koin.admin.abtest.exception.AbtestNotFoundException; +import in.koreatech.koin.admin.abtest.model.Abtest; + +public interface AbtestRepository extends Repository { + + Optional findById(Integer id); + + Abtest save(Abtest build); + + default Abtest getById(Integer id) { + return findById(id).orElseThrow(() -> + AbtestNotFoundException.withDetail("AbtestId: " + id)); + } + + Optional findByTitle(String title); + + default Abtest getByTitle(String title) { + return findByTitle(title).orElseThrow(() -> AbtestNotFoundException.withDetail("Abtest Title: " + title)); + } + + Long countBy(); + + Page findAll(Pageable pageable); + + void deleteById(Integer abtestId); +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/repository/AbtestVariableAssignRepository.java b/src/main/java/in/koreatech/koin/admin/abtest/repository/AbtestVariableAssignRepository.java new file mode 100644 index 000000000..e36b4035b --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/repository/AbtestVariableAssignRepository.java @@ -0,0 +1,27 @@ +package in.koreatech.koin.admin.abtest.repository; + +import static in.koreatech.koin.admin.abtest.model.redis.AbtestVariableAssign.DELIMITER; + +import java.util.Optional; + +import org.springframework.data.repository.Repository; + +import in.koreatech.koin.admin.abtest.model.redis.AbtestVariableAssign; + +public interface AbtestVariableAssignRepository extends Repository { + + AbtestVariableAssign save(AbtestVariableAssign abtestVariableAssign); + + Optional findById(String id); + + void deleteById(String id); + + default Optional findByVariableIdAndAccessHistoryId(Integer variableId, + Integer accessHistoryId) { + return findById(variableId + DELIMITER + accessHistoryId); + } + + default void deleteByVariableIdAndAccessHistoryId(Integer variableId, Integer accessHistoryId) { + deleteById(variableId + DELIMITER + accessHistoryId); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/repository/AbtestVariableAssignTemplateRepository.java b/src/main/java/in/koreatech/koin/admin/abtest/repository/AbtestVariableAssignTemplateRepository.java new file mode 100644 index 000000000..75ddaff16 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/repository/AbtestVariableAssignTemplateRepository.java @@ -0,0 +1,27 @@ +package in.koreatech.koin.admin.abtest.repository; + +import static in.koreatech.koin.admin.abtest.model.redis.AbtestVariableAssign.DELIMITER; + +import java.util.Set; + +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class AbtestVariableAssignTemplateRepository { + + private final RedisTemplate redisTemplate; + + public void deleteAllByVariableId(Integer variableId) { + Set keys = redisTemplate.keys("AbtestVariableAssign:" + variableId + DELIMITER + "*"); + if (keys == null) { + return; + } + for (String s : keys) { + redisTemplate.delete(s); + } + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/repository/AbtestVariableCountRepository.java b/src/main/java/in/koreatech/koin/admin/abtest/repository/AbtestVariableCountRepository.java new file mode 100644 index 000000000..de36d3554 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/repository/AbtestVariableCountRepository.java @@ -0,0 +1,26 @@ +package in.koreatech.koin.admin.abtest.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.repository.CrudRepository; + +import in.koreatech.koin.admin.abtest.model.redis.AbtestVariableCount; + +public interface AbtestVariableCountRepository extends CrudRepository { + + List findAll(); + + AbtestVariableCount save(AbtestVariableCount abtestVariableCount); + + Optional findById(Integer id); + + default AbtestVariableCount findOrCreateIfNotExists(Integer id) { + return findById(id).orElseGet(() -> + save(AbtestVariableCount.builder() + .variableId(id) + .count(0) + .build()) + ); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/repository/AbtestVariableRepository.java b/src/main/java/in/koreatech/koin/admin/abtest/repository/AbtestVariableRepository.java new file mode 100644 index 000000000..2d386f506 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/repository/AbtestVariableRepository.java @@ -0,0 +1,18 @@ +package in.koreatech.koin.admin.abtest.repository; + +import java.util.Optional; + +import org.springframework.data.repository.Repository; + +import in.koreatech.koin.admin.abtest.exception.AbtestVariableNotFoundException; +import in.koreatech.koin.admin.abtest.model.AbtestVariable; + +public interface AbtestVariableRepository extends Repository { + + Optional findById(Integer variableId); + + default AbtestVariable getById(Integer variableId) { + return findById(variableId).orElseThrow(() -> + AbtestVariableNotFoundException.withDetail("AbtestVariable id: " + variableId)); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/repository/AccessHistoryAbtestVariableCustomRepository.java b/src/main/java/in/koreatech/koin/admin/abtest/repository/AccessHistoryAbtestVariableCustomRepository.java new file mode 100644 index 000000000..4b18aace5 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/repository/AccessHistoryAbtestVariableCustomRepository.java @@ -0,0 +1,10 @@ +package in.koreatech.koin.admin.abtest.repository; + +import java.util.List; + +public interface AccessHistoryAbtestVariableCustomRepository { + + List findIdsToMove(Integer fromVariableId, int limit); + + void updateVariableIds(List idsToUpdate, Integer toVariableId); +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/repository/AccessHistoryAbtestVariableCustomRepositoryImpl.java b/src/main/java/in/koreatech/koin/admin/abtest/repository/AccessHistoryAbtestVariableCustomRepositoryImpl.java new file mode 100644 index 000000000..f01e0297a --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/repository/AccessHistoryAbtestVariableCustomRepositoryImpl.java @@ -0,0 +1,33 @@ +package in.koreatech.koin.admin.abtest.repository; + +import java.util.List; + +import org.springframework.stereotype.Repository; + +import com.querydsl.jpa.impl.JPAQueryFactory; + +import in.koreatech.koin.admin.abtest.model.QAccessHistoryAbtestVariable; +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class AccessHistoryAbtestVariableCustomRepositoryImpl + implements AccessHistoryAbtestVariableCustomRepository { + + private final JPAQueryFactory queryFactory; + + public List findIdsToMove(Integer fromVariableId, int limit) { + return queryFactory.select(QAccessHistoryAbtestVariable.accessHistoryAbtestVariable.id) + .from(QAccessHistoryAbtestVariable.accessHistoryAbtestVariable) + .where(QAccessHistoryAbtestVariable.accessHistoryAbtestVariable.variable.id.eq(fromVariableId)) + .limit(limit) + .fetch(); + } + + public void updateVariableIds(List idsToUpdate, Integer toVariableId) { + queryFactory.update(QAccessHistoryAbtestVariable.accessHistoryAbtestVariable) + .set(QAccessHistoryAbtestVariable.accessHistoryAbtestVariable.variable.id, toVariableId) + .where(QAccessHistoryAbtestVariable.accessHistoryAbtestVariable.id.in(idsToUpdate)) + .execute(); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/repository/AccessHistoryAbtestVariableRepository.java b/src/main/java/in/koreatech/koin/admin/abtest/repository/AccessHistoryAbtestVariableRepository.java new file mode 100644 index 000000000..ec949df9f --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/repository/AccessHistoryAbtestVariableRepository.java @@ -0,0 +1,10 @@ +package in.koreatech.koin.admin.abtest.repository; + +import org.springframework.data.repository.Repository; + +import in.koreatech.koin.admin.abtest.model.AccessHistoryAbtestVariable; + +public interface AccessHistoryAbtestVariableRepository extends Repository { + + AccessHistoryAbtestVariable save(AccessHistoryAbtestVariable accessHistoryAbtestVariable); +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/repository/AccessHistoryRepository.java b/src/main/java/in/koreatech/koin/admin/abtest/repository/AccessHistoryRepository.java new file mode 100644 index 000000000..d56cc23d6 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/repository/AccessHistoryRepository.java @@ -0,0 +1,27 @@ +package in.koreatech.koin.admin.abtest.repository; + +import java.util.Optional; + +import org.springframework.data.repository.Repository; + +import in.koreatech.koin.admin.abtest.exception.AccessHistoryNotFoundException; +import in.koreatech.koin.admin.abtest.model.AccessHistory; + +public interface AccessHistoryRepository extends Repository { + + AccessHistory save(AccessHistory accessHistory); + + Optional findById(Integer id); + + Optional findByDeviceId(Integer deviceId); + + default AccessHistory getById(Integer accessHistoryId) { + return findById(accessHistoryId).orElseThrow(() -> + AccessHistoryNotFoundException.withDetail("accessHistoryId: " + accessHistoryId)); + } + + default AccessHistory getByDeviceId(Integer deviceId) { + return findByDeviceId(deviceId).orElseThrow(() -> + AccessHistoryNotFoundException.withDetail("deviceId: " + deviceId)); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/repository/DeviceRepository.java b/src/main/java/in/koreatech/koin/admin/abtest/repository/DeviceRepository.java new file mode 100644 index 000000000..5bb66db74 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/repository/DeviceRepository.java @@ -0,0 +1,23 @@ +package in.koreatech.koin.admin.abtest.repository; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.repository.Repository; + +import in.koreatech.koin.admin.abtest.exception.DeviceNotFoundException; +import in.koreatech.koin.admin.abtest.model.Device; + +public interface DeviceRepository extends Repository { + + Device save(Device device); + + Optional findById(Integer id); + + List findAllByUserId(Integer id); + + default Device getById(Integer id) { + return findById(id).orElseThrow(() -> + DeviceNotFoundException.withDetail("id: " + id)); + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/scheduler/AbtestScheduler.java b/src/main/java/in/koreatech/koin/admin/abtest/scheduler/AbtestScheduler.java new file mode 100644 index 000000000..18144ae95 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/scheduler/AbtestScheduler.java @@ -0,0 +1,25 @@ +package in.koreatech.koin.admin.abtest.scheduler; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import in.koreatech.koin.admin.abtest.service.AbtestService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +@RequiredArgsConstructor +public class AbtestScheduler { + + private final AbtestService abtestService; + + @Scheduled(cron = "0 0 * * * *") + public void syncCacheCountToDB() { + try { + abtestService.syncCacheCountToDB(); + } catch (Exception e) { + log.warn("AB test 편입 수 DB 동기화 과정에서 오류가 발생했습니다."); + } + } +} diff --git a/src/main/java/in/koreatech/koin/admin/abtest/service/AbtestService.java b/src/main/java/in/koreatech/koin/admin/abtest/service/AbtestService.java new file mode 100644 index 000000000..e0083f323 --- /dev/null +++ b/src/main/java/in/koreatech/koin/admin/abtest/service/AbtestService.java @@ -0,0 +1,349 @@ +package in.koreatech.koin.admin.abtest.service; + +import java.time.Clock; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import in.koreatech.koin.admin.abtest.dto.AbtestAdminAssignRequest; +import in.koreatech.koin.admin.abtest.dto.AbtestAssignRequest; +import in.koreatech.koin.admin.abtest.dto.AbtestAssignResponse; +import in.koreatech.koin.admin.abtest.dto.AbtestCloseRequest; +import in.koreatech.koin.admin.abtest.dto.AbtestDevicesResponse; +import in.koreatech.koin.admin.abtest.dto.AbtestRequest; +import in.koreatech.koin.admin.abtest.dto.AbtestResponse; +import in.koreatech.koin.admin.abtest.dto.AbtestUsersResponse; +import in.koreatech.koin.admin.abtest.dto.AbtestsResponse; +import in.koreatech.koin.admin.abtest.exception.AbtestAlreadyExistException; +import in.koreatech.koin.admin.abtest.exception.AbtestAssignedUserException; +import in.koreatech.koin.admin.abtest.exception.AbtestDuplicatedVariableException; +import in.koreatech.koin.admin.abtest.exception.AbtestNotAssignedUserException; +import in.koreatech.koin.admin.abtest.exception.AbtestNotInProgressException; +import in.koreatech.koin.admin.abtest.exception.AbtestWinnerNotDecidedException; +import in.koreatech.koin.admin.abtest.model.Abtest; +import in.koreatech.koin.admin.abtest.model.AbtestStatus; +import in.koreatech.koin.admin.abtest.model.AbtestVariable; +import in.koreatech.koin.admin.abtest.model.AccessHistory; +import in.koreatech.koin.admin.abtest.model.Device; +import in.koreatech.koin.admin.abtest.model.redis.AbtestVariableAssign; +import in.koreatech.koin.admin.abtest.model.redis.AbtestVariableCount; +import in.koreatech.koin.admin.abtest.repository.AbtestRepository; +import in.koreatech.koin.admin.abtest.repository.AbtestVariableAssignRepository; +import in.koreatech.koin.admin.abtest.repository.AbtestVariableAssignTemplateRepository; +import in.koreatech.koin.admin.abtest.repository.AbtestVariableCountRepository; +import in.koreatech.koin.admin.abtest.repository.AbtestVariableRepository; +import in.koreatech.koin.admin.abtest.repository.AccessHistoryAbtestVariableCustomRepository; +import in.koreatech.koin.admin.abtest.repository.AccessHistoryRepository; +import in.koreatech.koin.admin.abtest.repository.DeviceRepository; +import in.koreatech.koin.domain.user.model.User; +import in.koreatech.koin.domain.user.repository.UserRepository; +import in.koreatech.koin.global.model.Criteria; +import in.koreatech.koin.global.useragent.UserAgentInfo; +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class AbtestService { + + private final Clock clock; + private final EntityManager entityManager; + private final AbtestVariableCountRepository abtestVariableCountRepository; + private final AbtestRepository abtestRepository; + private final AbtestVariableRepository abtestVariableRepository; + private final AccessHistoryRepository accessHistoryRepository; + private final DeviceRepository deviceRepository; + private final UserRepository userRepository; + private final AbtestVariableAssignRepository abtestVariableAssignRepository; + private final AbtestVariableAssignTemplateRepository abtestVariableAssignTemplateRepository; + private final AccessHistoryAbtestVariableCustomRepository accessHistoryAbtestVariableCustomRepository; + + @Transactional + public AbtestResponse createAbtest(AbtestRequest request) { + if (abtestRepository.findByTitle(request.title()).isPresent()) { + throw AbtestAlreadyExistException.withDetail("title: " + request.title()); + } + Abtest saved = abtestRepository.save( + Abtest.builder() + .title(request.title()) + .displayTitle(request.displayTitle()) + .description(request.description()) + .creator(request.creator()) + .team(request.team()) + .status(AbtestStatus.IN_PROGRESS) + .build() + ); + saved.setVariables(request.variables(), entityManager); + return AbtestResponse.from(saved); + } + + @Transactional + public AbtestResponse putAbtest(Integer abtestId, AbtestRequest request) { + Abtest abtest = abtestRepository.getById(abtestId); + validateAbtestInProgress(abtest); + abtest.update( + request.displayTitle(), + request.creator(), + request.team(), + request.title(), + request.description(), + request.variables() + ); + return AbtestResponse.from(abtest); + } + + + private void deleteVariableAssignCache(Abtest abtest) { + abtest.getAbtestVariables().forEach(abtestVariable -> + abtestVariableAssignTemplateRepository.deleteAllByVariableId(abtestVariable.getId())); + } + + @Transactional + public void deleteAbtest(Integer abtestId) { + abtestRepository.findById(abtestId).ifPresent(saved -> { + syncCacheCountToDB(saved); + deleteCountCache(saved); + deleteVariableAssignCache(saved); + abtestRepository.deleteById(abtestId); + }); + } + + private void deleteCountCache(Abtest abtest) { + abtest.getAbtestVariables() + .forEach(abtestVariable -> abtestVariableCountRepository.deleteById(abtestVariable.getId())); + } + + public AbtestsResponse getAbtests(Integer page, Integer limit) { + Long title = abtestRepository.countBy(); + Criteria criteria = Criteria.of(page, limit, title.intValue()); + PageRequest pageRequest = PageRequest.of(criteria.getPage(), criteria.getLimit(), + Sort.by(Sort.Direction.DESC, "id")); + Page abtests = abtestRepository.findAll(pageRequest); + return AbtestsResponse.of(abtests, criteria); + } + + public AbtestResponse getAbtest(Integer abtestId) { + return AbtestResponse.from(abtestRepository.getById(abtestId)); + } + + @Transactional + public void closeAbtest(Integer abtestId, AbtestCloseRequest request) { + Abtest abtest = abtestRepository.getById(abtestId); + validateAbtestInProgress(abtest); + abtest.close(request.winnerName()); + syncCacheCountToDB(abtest); + deleteCountCache(abtest); + deleteVariableAssignCache(abtest); + } + + @Transactional + public AbtestAssignResponse assignVariable(Integer accessHistoryId, UserAgentInfo userAgentInfo, Integer userId, + AbtestAssignRequest request) { + Abtest abtest = abtestRepository.getByTitle(request.title()); + AccessHistory accessHistory = findOrCreateAccessHistory(accessHistoryId); + Optional winnerResponse = returnWinnerIfClosed(abtest); + if (winnerResponse.isPresent()) { + return AbtestAssignResponse.of(winnerResponse.get(), accessHistory); + } + validateAssignedUser(abtest, accessHistory.getId(), userId); + List cacheCount = loadCacheCount(abtest); + AbtestVariable variable = abtest.findAssignVariable(cacheCount); + if (userId != null) { + createDeviceIfNotExists(userId, userAgentInfo, accessHistory, abtest); + } + accessHistory.addAbtestVariable(variable); + countCacheUpdate(variable); + variableAssignCacheSave(variable, accessHistory.getId()); + accessHistory.updateLastAccessedAt(clock); + return AbtestAssignResponse.of(variable, accessHistory); + } + + // 기기를 다른 사용자가 사용한 이력이 있는 경우 기존 사용자의 캐시를 삭제 + private void removeBeforeUserCache(AccessHistory accessHistory, Abtest abtest) { + for (AbtestVariable removeVariable : accessHistory.getVariableBy(abtest)) { + abtestVariableAssignRepository.deleteByVariableIdAndAccessHistoryId(removeVariable.getId(), + accessHistory.getId()); + AbtestVariableCount countCache = abtestVariableCountRepository.findOrCreateIfNotExists( + removeVariable.getId()); + countCache.minusCount(); + abtestVariableCountRepository.save(countCache); + } + } + + private List loadCacheCount(Abtest abtest) { + return abtest.getAbtestVariables().stream() + .map(abtestVariable -> abtestVariableCountRepository.findOrCreateIfNotExists(abtestVariable.getId())) + .toList(); + } + + private void countCacheUpdate(AbtestVariable variable) { + AbtestVariableCount countCache = abtestVariableCountRepository.findOrCreateIfNotExists(variable.getId()); + countCache.addCount(); + abtestVariableCountRepository.save(countCache); + } + + private void variableAssignCacheSave(AbtestVariable variable, Integer accessHistoryId) { + abtestVariableAssignRepository.save(AbtestVariableAssign.of(variable.getId(), accessHistoryId)); + } + + @Transactional + public String getMyVariable(Integer accessHistoryId, UserAgentInfo userAgentInfo, Integer userId, String title) { + Abtest abtest = abtestRepository.getByTitle(title); + AccessHistory accessHistory = accessHistoryRepository.getById(accessHistoryId); + syncCacheCountToDB(abtest); + Optional winner = returnWinnerIfClosed(abtest); + if (winner.isPresent()) { + return winner.get().getName(); + } + if (userId != null) { + createDeviceIfNotExists(userId, userAgentInfo, accessHistory, abtest); + } + Optional cacheVariable = abtest.getAbtestVariables().stream() + .filter(abtestVariable -> + abtestVariableAssignRepository.findByVariableIdAndAccessHistoryId(abtestVariable.getId(), + accessHistory.getId()).isPresent()) + .findAny(); + if (cacheVariable.isEmpty()) { + AbtestVariable dbVariable = accessHistory.findVariableByAbtestId(abtest.getId()) + .orElseThrow(() -> AbtestNotAssignedUserException.withDetail("abtestId: " + abtest.getId() + ", " + + "accessHistoryId: " + accessHistory.getId())); + abtestVariableAssignRepository.save(AbtestVariableAssign.of(dbVariable.getId(), accessHistory.getId())); + return dbVariable.getName(); + } + accessHistory.updateLastAccessedAt(clock); + return cacheVariable.get().getName(); + } + + private void validateAssignedUser(Abtest abtest, Integer accessHistoryId, Integer userId) { + AccessHistory accessHistory = accessHistoryRepository.getById(accessHistoryId); + if (userId != null && accessHistory.getDevice() != null + && !Objects.equals(accessHistory.getDevice().getUser().getId(), userId)) { + return; + } + if (abtest.getAbtestVariables().stream() + .anyMatch(abtestVariable -> accessHistory.hasVariable(abtestVariable.getId()))) { + throw AbtestAssignedUserException.withDetail( + "abtestId: " + abtest.getId() + ", accessHistoryId: " + accessHistoryId); + } + } + + @Transactional + public void syncCacheCountToDB() { + List cacheCount = abtestVariableCountRepository.findAll(); + cacheCount.removeIf(Objects::isNull); + cacheCount.forEach(abtestVariableCount -> { + Optional variable = abtestVariableRepository.findById(abtestVariableCount.getVariableId()); + if (variable.isEmpty()) { + abtestVariableCountRepository.deleteById(abtestVariableCount.getVariableId()); + return; + } + variable.get().addCount(abtestVariableCount.getCount()); + abtestVariableCount.resetCount(); + }); + abtestVariableCountRepository.saveAll(cacheCount); + } + + public void syncCacheCountToDB(Abtest abtest) { + List cacheCount = abtest.getAbtestVariables().stream() + .map(AbtestVariable::getId) + .map(abtestVariableCountRepository::findById) + .filter(Optional::isPresent) + .map(Optional::get) + .toList(); + cacheCount.forEach(abtestVariableCount -> { + AbtestVariable variable = abtestVariableRepository.getById(abtestVariableCount.getVariableId()); + variable.addCount(abtestVariableCount.getCount()); + abtestVariableCount.resetCount(); + }); + abtestVariableCountRepository.saveAll(cacheCount); + } + + public AbtestUsersResponse getUsersByName(String userName) { + return AbtestUsersResponse.from(userRepository.findAllByName(userName)); + } + + public AbtestDevicesResponse getDevicesByUserId(Integer userId) { + User saved = userRepository.getById(userId); + return AbtestDevicesResponse.from(deviceRepository.findAllByUserId(saved.getId())); + } + + @Transactional + public void assignVariableByAdmin(Integer abtestId, AbtestAdminAssignRequest request) { + Abtest abtest = abtestRepository.getById(abtestId); + validateAbtestInProgress(abtest); + AccessHistory accessHistory = accessHistoryRepository.getByDeviceId(request.deviceId()); + Optional beforeVariable = abtest.findVariableByAccessHistory(accessHistory); + AbtestVariable afterVariable = abtest.getVariableByName(request.variableName()); + validateDuplicatedVariables(beforeVariable, afterVariable); + abtest.assignVariableByAdmin(accessHistory, request.variableName()); + beforeVariable.ifPresent( + abtestVariable -> abtestVariableAssignRepository.deleteByVariableIdAndAccessHistoryId( + abtestVariable.getId(), + accessHistory.getId())); + variableAssignCacheSave(afterVariable, accessHistory.getId()); + } + + private static void validateDuplicatedVariables(Optional beforeVariable, + AbtestVariable afterVariable) { + if (beforeVariable.isEmpty()) { + return; + } + if (Objects.equals(beforeVariable.get().getId(), afterVariable.getId())) { + throw AbtestDuplicatedVariableException.withDetail("beforeVariable id: " + beforeVariable.get().getId() + + ", afterVariable id: " + afterVariable.getId()); + } + } + + private static void validateAbtestInProgress(Abtest abtest) { + if (abtest.getStatus() != AbtestStatus.IN_PROGRESS) { + throw AbtestNotInProgressException.withDetail("abtestId: " + abtest.getId()); + } + } + + private static Optional returnWinnerIfClosed(Abtest abtest) { + if (abtest.getStatus() == AbtestStatus.CLOSED) { + if (abtest.getWinner() != null) { + return Optional.of(abtest.getWinner()); + } + throw AbtestWinnerNotDecidedException.withDetail("abtestId: " + abtest.getId()); + } + return Optional.empty(); + } + + public AccessHistory findOrCreateAccessHistory(Integer id) { + if (id == null) { + return accessHistoryRepository.save(AccessHistory.builder().build()); + } + return accessHistoryRepository.getById(id); + } + + private void createDeviceIfNotExists(Integer userId, UserAgentInfo userAgentInfo, + AccessHistory accessHistory, Abtest abtest) { + userRepository.getById(userId); + if (accessHistory.getDevice() == null) { + Device device = deviceRepository.save( + Device.builder() + .user(userRepository.getById(userId)) + .model(userAgentInfo.getModel()) + .type(userAgentInfo.getType()) + .build() + ); + accessHistory.connectDevice(device); + } + Device device = accessHistory.getDevice(); + if (device.getModel() == null || device.getType() == null) { + device.setModelInfo(userAgentInfo.getModel(), userAgentInfo.getType()); + } + if (!Objects.equals(device.getUser().getId(), userId)) { + device.changeUser(userRepository.getById(userId)); + removeBeforeUserCache(accessHistory, abtest); + } + } +} diff --git a/src/main/java/in/koreatech/koin/admin/shop/dto/AdminCreateMenuRequest.java b/src/main/java/in/koreatech/koin/admin/shop/dto/AdminCreateMenuRequest.java index 3edf2fe85..d51030170 100644 --- a/src/main/java/in/koreatech/koin/admin/shop/dto/AdminCreateMenuRequest.java +++ b/src/main/java/in/koreatech/koin/admin/shop/dto/AdminCreateMenuRequest.java @@ -37,7 +37,8 @@ public record AdminCreateMenuRequest( """, description = "이미지 URL 리스트", requiredMode = REQUIRED) @Size(max = 3, message = "이미지는 최대 3개까지 입력 가능합니다.") @UniqueUrl(message = "이미지 URL은 중복될 수 없습니다.") - @NotBlankElement(message = "이미지 URL은 필수입니다.") + @NotNull(message = "이미지 URL은 null일 수 없습니다.") + @NotBlankElement(message = "빈 요소가 존재할 수 없습니다.") List imageUrls, @Schema(example = "true", description = "단일 메뉴 여부", requiredMode = REQUIRED) diff --git a/src/main/java/in/koreatech/koin/admin/shop/dto/AdminCreateShopRequest.java b/src/main/java/in/koreatech/koin/admin/shop/dto/AdminCreateShopRequest.java index d524b5683..c2b917dea 100644 --- a/src/main/java/in/koreatech/koin/admin/shop/dto/AdminCreateShopRequest.java +++ b/src/main/java/in/koreatech/koin/admin/shop/dto/AdminCreateShopRequest.java @@ -55,7 +55,8 @@ public record AdminCreateShopRequest( [ "https://static.koreatech.in/example.png" ] """, requiredMode = NOT_REQUIRED) @UniqueUrl(message = "이미지 URL은 중복될 수 없습니다.") - @NotBlankElement + @NotNull(message = "이미지 URL은 null일 수 없습니다.") + @NotBlankElement(message = "빈 요소가 존재할 수 없습니다.") List imageUrls, @Schema(description = "이름", example = "수신반점", requiredMode = REQUIRED) diff --git a/src/main/java/in/koreatech/koin/admin/shop/dto/AdminModifyMenuRequest.java b/src/main/java/in/koreatech/koin/admin/shop/dto/AdminModifyMenuRequest.java index 6fd44b4dd..117b396ba 100644 --- a/src/main/java/in/koreatech/koin/admin/shop/dto/AdminModifyMenuRequest.java +++ b/src/main/java/in/koreatech/koin/admin/shop/dto/AdminModifyMenuRequest.java @@ -35,7 +35,8 @@ public record AdminModifyMenuRequest( """, description = "이미지 URL 리스트", requiredMode = NOT_REQUIRED) @Size(max = 3, message = "이미지는 최대 3개까지 입력 가능합니다.") @UniqueUrl(message = "이미지 URL은 중복될 수 없습니다.") - @NotBlankElement(message = "이미지 URL은 필수입니다.") + @NotNull(message = "이미지 URL은 null일 수 없습니다.") + @NotBlankElement(message = "빈 요소가 존재할 수 없습니다.") List imageUrls, @Schema(example = "true", description = "단일 메뉴 여부", requiredMode = REQUIRED) diff --git a/src/main/java/in/koreatech/koin/admin/shop/dto/AdminModifyShopRequest.java b/src/main/java/in/koreatech/koin/admin/shop/dto/AdminModifyShopRequest.java index d6d6c5b21..16b8c6d85 100644 --- a/src/main/java/in/koreatech/koin/admin/shop/dto/AdminModifyShopRequest.java +++ b/src/main/java/in/koreatech/koin/admin/shop/dto/AdminModifyShopRequest.java @@ -55,7 +55,8 @@ public record AdminModifyShopRequest( [ "https://static.koreatech.in/example.png" ] """, requiredMode = NOT_REQUIRED) @UniqueUrl(message = "이미지 URL은 중복될 수 없습니다.") - @NotBlankElement + @NotNull(message = "이미지 URL은 null일 수 없습니다.") + @NotBlankElement(message = "빈 요소가 존재할 수 없습니다.") List imageUrls, @Schema(description = "이름", example = "수신반점", requiredMode = REQUIRED) diff --git a/src/main/java/in/koreatech/koin/admin/shop/service/AdminShopService.java b/src/main/java/in/koreatech/koin/admin/shop/service/AdminShopService.java index 777b6b6dc..2df999e4d 100644 --- a/src/main/java/in/koreatech/koin/admin/shop/service/AdminShopService.java +++ b/src/main/java/in/koreatech/koin/admin/shop/service/AdminShopService.java @@ -111,14 +111,14 @@ public void createShop(AdminCreateShopRequest adminCreateShopRequest) { .shop(savedShop) .name(categoryName) .build(); - adminMenuCategoryRepository.save(menuCategory); + savedShop.getMenuCategories().add(menuCategory); } for (String imageUrl : adminCreateShopRequest.imageUrls()) { ShopImage shopImage = ShopImage.builder() .shop(savedShop) .imageUrl(imageUrl) .build(); - adminShopImageRepository.save(shopImage); + savedShop.getShopImages().add(shopImage); } for (InnerShopOpen open : adminCreateShopRequest.open()) { ShopOpen shopOpen = ShopOpen.builder() @@ -128,7 +128,7 @@ public void createShop(AdminCreateShopRequest adminCreateShopRequest) { .dayOfWeek(open.dayOfWeek()) .closed(open.closed()) .build(); - adminShopOpenRepository.save(shopOpen); + savedShop.getShopOpens().add(shopOpen); } List categories = adminShopCategoryRepository.findAllByIdIn(adminCreateShopRequest.categoryIds()); for (ShopCategory shopCategory : categories) { @@ -136,7 +136,7 @@ public void createShop(AdminCreateShopRequest adminCreateShopRequest) { .shopCategory(shopCategory) .shop(savedShop) .build(); - adminShopCategoryMapRepository.save(shopCategoryMap); + savedShop.getShopCategories().add(shopCategoryMap); } } @@ -160,14 +160,14 @@ public void createMenu(Integer shopId, AdminCreateMenuRequest adminCreateMenuReq .menuCategory(menuCategory) .menu(savedMenu) .build(); - adminMenuCategoryMapRepository.save(menuCategoryMap); + savedMenu.getMenuCategoryMaps().add(menuCategoryMap); } for (String imageUrl : adminCreateMenuRequest.imageUrls()) { MenuImage menuImage = MenuImage.builder() .imageUrl(imageUrl) .menu(savedMenu) .build(); - adminMenuImageRepository.save(menuImage); + savedMenu.getMenuImages().add(menuImage); } if (adminCreateMenuRequest.optionPrices() == null) { MenuOption menuOption = MenuOption.builder() @@ -175,7 +175,7 @@ public void createMenu(Integer shopId, AdminCreateMenuRequest adminCreateMenuReq .price(adminCreateMenuRequest.singlePrice()) .menu(menu) .build(); - adminMenuDetailRepository.save(menuOption); + savedMenu.getMenuOptions().add(menuOption); } else { for (var option : adminCreateMenuRequest.optionPrices()) { MenuOption menuOption = MenuOption.builder() @@ -183,7 +183,7 @@ public void createMenu(Integer shopId, AdminCreateMenuRequest adminCreateMenuReq .price(option.price()) .menu(menu) .build(); - adminMenuDetailRepository.save(menuOption); + savedMenu.getMenuOptions().add(menuOption); } } } diff --git a/src/main/java/in/koreatech/koin/domain/community/article/controller/CommunityApi.java b/src/main/java/in/koreatech/koin/domain/community/article/controller/ArticleApi.java similarity index 90% rename from src/main/java/in/koreatech/koin/domain/community/article/controller/CommunityApi.java rename to src/main/java/in/koreatech/koin/domain/community/article/controller/ArticleApi.java index 004b8a410..3c449b328 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/controller/CommunityApi.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/controller/ArticleApi.java @@ -7,6 +7,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import in.koreatech.koin.domain.community.article.dto.ArticleHotKeywordResponse; @@ -22,8 +23,9 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; -@Tag(name = "(Normal) Community: 커뮤니티", description = "커뮤니티 정보를 관리한다") -public interface CommunityApi { +@Tag(name = "(Normal) Articles: 게시글", description = "게시글 정보를 관리한다") +@RequestMapping("/articles") +public interface ArticleApi { @ApiResponses( value = { @@ -32,7 +34,7 @@ public interface CommunityApi { } ) @Operation(summary = "게시글 단건 조회") - @GetMapping("/articles/{id}") + @GetMapping("/{id}") ResponseEntity getArticle( @RequestParam(required = false) Integer boardId, @Parameter(in = PATH) @PathVariable("id") Integer articleId @@ -45,7 +47,7 @@ ResponseEntity getArticle( } ) @Operation(summary = "게시글 목록 조회") - @GetMapping("/articles") + @GetMapping("") ResponseEntity getArticles( @RequestParam Integer boardId, @RequestParam(required = false) Integer page, @@ -59,7 +61,7 @@ ResponseEntity getArticles( } ) @Operation(summary = "인기 게시글 목록 조회") - @GetMapping("/articles/hot") + @GetMapping("/hot") ResponseEntity> getHotArticles(); @ApiResponses( @@ -69,7 +71,7 @@ ResponseEntity getArticles( } ) @Operation(summary = "게시글 검색") - @GetMapping("/articles/search") + @GetMapping("/search") ResponseEntity searchArticles( @RequestParam String query, @RequestParam(required = false) Integer boardId, @@ -84,7 +86,7 @@ ResponseEntity searchArticles( } ) @Operation(summary = "많이 검색되는 키워드(검색 화면)") - @GetMapping("/articles/hot/keyword") + @GetMapping("/hot/keyword") ResponseEntity getArticlesHotKeyword( @RequestParam Integer count ); diff --git a/src/main/java/in/koreatech/koin/domain/community/article/controller/CommunityController.java b/src/main/java/in/koreatech/koin/domain/community/article/controller/ArticleController.java similarity index 77% rename from src/main/java/in/koreatech/koin/domain/community/article/controller/CommunityController.java rename to src/main/java/in/koreatech/koin/domain/community/article/controller/ArticleController.java index 5f5f51501..2b66f9efe 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/controller/CommunityController.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/controller/ArticleController.java @@ -13,23 +13,23 @@ import in.koreatech.koin.domain.community.article.dto.ArticleResponse; import in.koreatech.koin.domain.community.article.dto.ArticlesResponse; import in.koreatech.koin.domain.community.article.dto.HotArticleItemResponse; -import in.koreatech.koin.domain.community.article.service.CommunityService; +import in.koreatech.koin.domain.community.article.service.ArticleService; import in.koreatech.koin.global.ipaddress.IpAddress; import lombok.RequiredArgsConstructor; @RestController @RequiredArgsConstructor @RequestMapping("/articles") -public class CommunityController implements CommunityApi { +public class ArticleController implements ArticleApi { - private final CommunityService communityService; + private final ArticleService articleService; @GetMapping("/{id}") public ResponseEntity getArticle( @RequestParam(required = false) Integer boardId, @PathVariable("id") Integer articleId ) { - ArticleResponse foundArticle = communityService.getArticle(boardId, articleId); + ArticleResponse foundArticle = articleService.getArticle(boardId, articleId); return ResponseEntity.ok().body(foundArticle); } @@ -39,13 +39,13 @@ public ResponseEntity getArticles( @RequestParam(required = false) Integer page, @RequestParam(required = false) Integer limit ) { - ArticlesResponse foundArticles = communityService.getArticles(boardId, page, limit); + ArticlesResponse foundArticles = articleService.getArticles(boardId, page, limit); return ResponseEntity.ok().body(foundArticles); } @GetMapping("/hot") public ResponseEntity> getHotArticles() { - List hotArticles = communityService.getHotArticles(); + List hotArticles = articleService.getHotArticles(); return ResponseEntity.ok().body(hotArticles); } @@ -57,7 +57,7 @@ public ResponseEntity searchArticles( @RequestParam(required = false) Integer limit, @IpAddress String ipAddress ) { - ArticlesResponse foundArticles = communityService.searchArticles(query, boardId, page, limit, ipAddress); + ArticlesResponse foundArticles = articleService.searchArticles(query, boardId, page, limit, ipAddress); return ResponseEntity.ok().body(foundArticles); } @@ -65,7 +65,7 @@ public ResponseEntity searchArticles( public ResponseEntity getArticlesHotKeyword( @RequestParam Integer count ) { - ArticleHotKeywordResponse response = communityService.getArticlesHotKeyword(count); + ArticleHotKeywordResponse response = articleService.getArticlesHotKeyword(count); return ResponseEntity.ok().body(response); } } diff --git a/src/main/java/in/koreatech/koin/domain/community/article/dto/ArticleResponse.java b/src/main/java/in/koreatech/koin/domain/community/article/dto/ArticleResponse.java index c6a61ab10..a578b8439 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/dto/ArticleResponse.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/dto/ArticleResponse.java @@ -58,7 +58,7 @@ public static ArticleResponse of(Article article) { article.getTitle(), article.getContent(), article.getAuthor(), - article.getHit(), + article.getTotalHit(), article.getAttachments().stream() .map(InnerArticleAttachmentResponse::from) .toList(), diff --git a/src/main/java/in/koreatech/koin/domain/community/article/dto/ArticlesResponse.java b/src/main/java/in/koreatech/koin/domain/community/article/dto/ArticlesResponse.java index 5d3b36197..349af047b 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/dto/ArticlesResponse.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/dto/ArticlesResponse.java @@ -77,7 +77,7 @@ public static InnerArticleResponse from(Article article) { article.getBoard().getId(), article.getTitle(), article.getAuthor(), - article.getHit(), + article.getTotalHit(), article.getRegisteredAt(), article.getUpdatedAt() ); diff --git a/src/main/java/in/koreatech/koin/domain/community/article/dto/HotArticleItemResponse.java b/src/main/java/in/koreatech/koin/domain/community/article/dto/HotArticleItemResponse.java index ab38d7119..4aa2f2988 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/dto/HotArticleItemResponse.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/dto/HotArticleItemResponse.java @@ -43,7 +43,7 @@ public static HotArticleItemResponse from(Article article) { article.getBoard().getId(), article.getTitle(), article.getAuthor(), - article.getHit(), + article.getTotalHit(), article.getRegisteredAt(), article.getUpdatedAt() ); diff --git a/src/main/java/in/koreatech/koin/domain/community/article/model/Article.java b/src/main/java/in/koreatech/koin/domain/community/article/model/Article.java index 9b1491fef..664344a70 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/model/Article.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/model/Article.java @@ -107,6 +107,10 @@ public void increaseKoinHit() { koinHit++; } + public int getTotalHit() { + return hit + koinHit; + } + public void setPrevNextArticles(Article prev, Article next) { if (prev != null) { prevId = prev.getId(); diff --git a/src/main/java/in/koreatech/koin/domain/community/article/model/redis/ArticleHit.java b/src/main/java/in/koreatech/koin/domain/community/article/model/redis/ArticleHit.java new file mode 100644 index 000000000..4cb2530d4 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/community/article/model/redis/ArticleHit.java @@ -0,0 +1,31 @@ +package in.koreatech.koin.domain.community.article.model.redis; + +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; + +import in.koreatech.koin.domain.community.article.model.Article; +import lombok.Builder; +import lombok.Getter; + +@Getter +@RedisHash("articleHit") +public class ArticleHit { + + @Id + private Integer id; + + private Integer hit; + + @Builder + private ArticleHit(Integer id, Integer hit) { + this.id = id; + this.hit = hit; + } + + public static ArticleHit from(Article article) { + return ArticleHit.builder() + .id(article.getId()) + .hit(article.getTotalHit()) + .build(); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/community/article/repository/ArticleRepository.java b/src/main/java/in/koreatech/koin/domain/community/article/repository/ArticleRepository.java index 7a221990c..93604ee54 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/repository/ArticleRepository.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/repository/ArticleRepository.java @@ -1,7 +1,8 @@ package in.koreatech.koin.domain.community.article.repository; -import static in.koreatech.koin.domain.community.article.service.CommunityService.NOTICE_BOARD_ID; +import static in.koreatech.koin.domain.community.article.service.ArticleService.NOTICE_BOARD_ID; +import java.time.LocalDate; import java.util.List; import java.util.Optional; @@ -75,4 +76,11 @@ default Article getNextArticle(Board board, Article article) { } return findNextArticle(article.getId(), board.getId()).orElse(null); } + + @Query(value = "SELECT * FROM koreatech_articles a WHERE a.registered_at > :registeredAt " + + "ORDER BY (a.hit + a.koin_hit) DESC, a.registered_at DESC, a.id DESC " + + "LIMIT :limit", nativeQuery = true) + List
findAllHotArticles(@Param("registeredAt") LocalDate registeredAt, @Param("limit") int limit); + + List
findAllByRegisteredAtIsAfter(LocalDate localDate); } diff --git a/src/main/java/in/koreatech/koin/domain/community/article/repository/redis/ArticleHitRepository.java b/src/main/java/in/koreatech/koin/domain/community/article/repository/redis/ArticleHitRepository.java new file mode 100644 index 000000000..e018368e8 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/community/article/repository/redis/ArticleHitRepository.java @@ -0,0 +1,12 @@ +package in.koreatech.koin.domain.community.article.repository.redis; + +import java.util.List; + +import org.springframework.data.repository.CrudRepository; + +import in.koreatech.koin.domain.community.article.model.redis.ArticleHit; + +public interface ArticleHitRepository extends CrudRepository { + + List findAll(); +} diff --git a/src/main/java/in/koreatech/koin/domain/community/article/repository/redis/HotArticleRepository.java b/src/main/java/in/koreatech/koin/domain/community/article/repository/redis/HotArticleRepository.java new file mode 100644 index 000000000..4ab41b8b6 --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/community/article/repository/redis/HotArticleRepository.java @@ -0,0 +1,46 @@ +package in.koreatech.koin.domain.community.article.repository.redis; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +import org.springframework.data.redis.core.ListOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class HotArticleRepository { + + public static final String REDIS_KEY = "hotArticles"; + + private final RedisTemplate redisTemplate; + + public void saveArticlesWithHitToRedis(Map articlesWithHit, int limit) { + List sortedArticles = articlesWithHit.entrySet() + .stream() + .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())) + .limit(limit) + .map(Map.Entry::getKey) + .toList(); + ListOperations listOps = redisTemplate.opsForList(); + redisTemplate.delete(REDIS_KEY); + for (Integer articleId : sortedArticles) { + listOps.rightPush(REDIS_KEY, articleId); + } + } + + public List getHotArticles(int limit) { + ListOperations listOps = redisTemplate.opsForList(); + List cache = listOps.range(REDIS_KEY, 0, -1); + if (cache == null) { + return List.of(); + } + return cache.stream() + .map(Integer.class::cast) + .limit(limit) + .toList(); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/community/article/scheduler/ArticleScheduler.java b/src/main/java/in/koreatech/koin/domain/community/article/scheduler/ArticleScheduler.java index 6e1b02df2..579172cd3 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/scheduler/ArticleScheduler.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/scheduler/ArticleScheduler.java @@ -3,7 +3,7 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; -import in.koreatech.koin.domain.community.article.service.CommunityService; +import in.koreatech.koin.domain.community.article.service.ArticleService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -12,12 +12,21 @@ @RequiredArgsConstructor public class ArticleScheduler { - private final CommunityService communityService; + private final ArticleService articleService; + + @Scheduled(cron = "0 0 6 * * *") + public void updateHotArticles() { + try { + articleService.updateHotArticles(); + } catch (Exception e) { + log.error("인기 게시글 업데이트 중에 오류가 발생했습니다.", e); + } + } @Scheduled(cron = "0 0 0/6 * * *") public void resetOldKeywordsAndIpMaps() { try { - communityService.resetWeightsAndCounts(); + articleService.resetWeightsAndCounts(); } catch (Exception e) { log.error("많이 검색한 키워드 초기화 중에 오류가 발생했습니다.", e); } diff --git a/src/main/java/in/koreatech/koin/domain/community/article/service/CommunityService.java b/src/main/java/in/koreatech/koin/domain/community/article/service/ArticleService.java similarity index 76% rename from src/main/java/in/koreatech/koin/domain/community/article/service/CommunityService.java rename to src/main/java/in/koreatech/koin/domain/community/article/service/ArticleService.java index 737ca6c91..851053ed5 100644 --- a/src/main/java/in/koreatech/koin/domain/community/article/service/CommunityService.java +++ b/src/main/java/in/koreatech/koin/domain/community/article/service/ArticleService.java @@ -1,9 +1,14 @@ package in.koreatech.koin.domain.community.article.service; +import java.time.Clock; +import java.time.LocalDate; import java.time.LocalDateTime; -import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -21,11 +26,13 @@ import in.koreatech.koin.domain.community.article.model.ArticleSearchKeyword; import in.koreatech.koin.domain.community.article.model.ArticleSearchKeywordIpMap; import in.koreatech.koin.domain.community.article.model.Board; +import in.koreatech.koin.domain.community.article.model.redis.ArticleHit; import in.koreatech.koin.domain.community.article.repository.ArticleRepository; import in.koreatech.koin.domain.community.article.repository.ArticleSearchKeywordIpMapRepository; import in.koreatech.koin.domain.community.article.repository.ArticleSearchKeywordRepository; import in.koreatech.koin.domain.community.article.repository.BoardRepository; -import in.koreatech.koin.domain.user.repository.UserRepository; +import in.koreatech.koin.domain.community.article.repository.redis.ArticleHitRepository; +import in.koreatech.koin.domain.community.article.repository.redis.HotArticleRepository; import in.koreatech.koin.global.exception.KoinIllegalArgumentException; import in.koreatech.koin.global.model.Criteria; import lombok.RequiredArgsConstructor; @@ -33,9 +40,10 @@ @Service @RequiredArgsConstructor @Transactional(readOnly = true) -public class CommunityService { +public class ArticleService { public static final int NOTICE_BOARD_ID = 4; + private static final int HOT_ARTICLE_BEFORE_DAYS = 30; private static final int HOT_ARTICLE_LIMIT = 10; private static final int MAXIMUM_SEARCH_LENGTH = 100; private static final Sort ARTICLES_SORT = Sort.by( @@ -45,14 +53,17 @@ public class CommunityService { private final ArticleRepository articleRepository; private final BoardRepository boardRepository; - private final UserRepository userRepository; private final ArticleSearchKeywordIpMapRepository articleSearchKeywordIpMapRepository; private final ArticleSearchKeywordRepository articleSearchKeywordRepository; + private final ArticleHitRepository articleHitRepository; + private final HotArticleRepository hotArticleRepository; + private final Clock clock; @Transactional public ArticleResponse getArticle(Integer boardId, Integer articleId) { Article article = articleRepository.getById(articleId); - // article.increaseHit(); + //TODO: 추후(Device 관련 로직 구현 후) 조회수 증가 제한 로직 부가 필요 + article.increaseKoinHit(); Board board = getBoard(boardId, article); Article prevArticle = articleRepository.getPreviousArticle(board, article); Article nextArticle = articleRepository.getNextArticle(board, article); @@ -85,16 +96,24 @@ public ArticlesResponse getArticles(Integer boardId, Integer page, Integer limit } public List getHotArticles() { - PageRequest pageRequest = PageRequest.of(0, HOT_ARTICLE_LIMIT, ARTICLES_SORT); - return articleRepository.findAll(pageRequest).stream() - .sorted(Comparator.comparing(Article::getHit).reversed()) - .map(HotArticleItemResponse::from) - .toList(); + List
cacheList = hotArticleRepository.getHotArticles(HOT_ARTICLE_LIMIT).stream() + .map(articleRepository::getById) + .collect(Collectors.toList()); + if (cacheList.size() < HOT_ARTICLE_LIMIT) { + List
highestHitArticles = articleRepository.findAllHotArticles( + LocalDate.now(clock).minusDays(HOT_ARTICLE_BEFORE_DAYS), HOT_ARTICLE_LIMIT); + cacheList.addAll(highestHitArticles); + return cacheList.stream().limit(HOT_ARTICLE_LIMIT) + .map(HotArticleItemResponse::from) + .toList(); + } + return cacheList.stream().map(HotArticleItemResponse::from).toList(); } @Transactional - public ArticlesResponse searchArticles(String query, Integer boardId, Integer page, Integer limit, String ipAddress) { - if(query.length() >= MAXIMUM_SEARCH_LENGTH) { + public ArticlesResponse searchArticles(String query, Integer boardId, Integer page, Integer limit, + String ipAddress) { + if (query.length() >= MAXIMUM_SEARCH_LENGTH) { throw new KoinIllegalArgumentException("검색어의 최대 길이를 초과했습니다."); } @@ -147,7 +166,8 @@ private void saveOrUpdateSearchLog(String query, String ipAddress) { return newKeyword; }); - ArticleSearchKeywordIpMap map = articleSearchKeywordIpMapRepository.findByArticleSearchKeywordAndIpAddress(keyword, ipAddress) + ArticleSearchKeywordIpMap map = articleSearchKeywordIpMapRepository.findByArticleSearchKeywordAndIpAddress( + keyword, ipAddress) .orElseGet(() -> { ArticleSearchKeywordIpMap newMap = ArticleSearchKeywordIpMap.builder() .articleSearchKeyword(keyword) @@ -210,4 +230,26 @@ public void resetWeightsAndCounts() { ipMap.resetSearchCount(); } } + + @Transactional + public void updateHotArticles() { + List articleHits = articleHitRepository.findAll(); + articleHitRepository.deleteAll(); + List
allArticles = + articleRepository.findAllByRegisteredAtIsAfter(LocalDate.now(clock).minusDays(HOT_ARTICLE_BEFORE_DAYS)); + articleHitRepository.saveAll(allArticles.stream().map(ArticleHit::from).toList()); + + Map articlesIdWithHit = new HashMap<>(); + for (Article article : allArticles) { + Optional cache = articleHits.stream() + .filter(articleHit -> articleHit.getId().equals(article.getId())) + .findAny(); + int beforeArticleHit = cache.isPresent() ? cache.get().getHit() : 0; + if (article.getTotalHit() - beforeArticleHit <= 0) { + continue; + } + articlesIdWithHit.put(article.getId(), article.getTotalHit() - beforeArticleHit); + } + hotArticleRepository.saveArticlesWithHitToRedis(articlesIdWithHit, HOT_ARTICLE_LIMIT); + } } diff --git a/src/main/java/in/koreatech/koin/domain/coop/service/CoopService.java b/src/main/java/in/koreatech/koin/domain/coop/service/CoopService.java index 205456286..353998a97 100644 --- a/src/main/java/in/koreatech/koin/domain/coop/service/CoopService.java +++ b/src/main/java/in/koreatech/koin/domain/coop/service/CoopService.java @@ -71,7 +71,6 @@ public void saveDiningImage(DiningImageRequest imageRequest) { if (isOpened && !isImageExist) { eventPublisher.publishEvent(new DiningImageUploadEvent(dining.getId(), dining.getImageUrl())); } - dining.setImageUrl(imageRequest.imageUrl()); } diff --git a/src/main/java/in/koreatech/koin/domain/ownershop/dto/OwnerShopsRequest.java b/src/main/java/in/koreatech/koin/domain/ownershop/dto/OwnerShopsRequest.java index 20bd94c78..0429bc683 100644 --- a/src/main/java/in/koreatech/koin/domain/ownershop/dto/OwnerShopsRequest.java +++ b/src/main/java/in/koreatech/koin/domain/ownershop/dto/OwnerShopsRequest.java @@ -51,7 +51,8 @@ public record OwnerShopsRequest( [ "https://testimage.com" ] """, requiredMode = REQUIRED) @UniqueUrl(message = "이미지 URL은 중복될 수 없습니다.") - @NotBlankElement(message = "빈 요소가 존재합니다.") + @NotNull(message = "이미지 URL은 null일 수 없습니다.") + @NotBlankElement(message = "빈 요소가 존재할 수 없습니다.") List imageUrls, @Schema(description = "가게명", example = "써니 숯불 도시락", requiredMode = REQUIRED) diff --git a/src/main/java/in/koreatech/koin/domain/ownershop/service/OwnerShopService.java b/src/main/java/in/koreatech/koin/domain/ownershop/service/OwnerShopService.java index a96423a1a..5b9565ec0 100644 --- a/src/main/java/in/koreatech/koin/domain/ownershop/service/OwnerShopService.java +++ b/src/main/java/in/koreatech/koin/domain/ownershop/service/OwnerShopService.java @@ -97,14 +97,14 @@ public void createOwnerShops(Integer ownerId, OwnerShopsRequest ownerShopsReques .shop(savedShop) .name(categoryName) .build(); - menuCategoryRepository.save(menuCategory); + savedShop.getMenuCategories().add(menuCategory); } for (String imageUrl : ownerShopsRequest.imageUrls()) { ShopImage shopImage = ShopImage.builder() .shop(savedShop) .imageUrl(imageUrl) .build(); - shopImageRepository.save(shopImage); + savedShop.getShopImages().add(shopImage); } for (OwnerShopsRequest.InnerOpenRequest open : ownerShopsRequest.open()) { ShopOpen shopOpen = ShopOpen.builder() @@ -114,7 +114,7 @@ public void createOwnerShops(Integer ownerId, OwnerShopsRequest ownerShopsReques .dayOfWeek(open.dayOfWeek()) .closed(open.closed()) .build(); - shopOpenRepository.save(shopOpen); + savedShop.getShopOpens().add(shopOpen); } List shopCategories = shopCategoryRepository.findAllByIdIn(ownerShopsRequest.categoryIds()); for (ShopCategory shopCategory : shopCategories) { @@ -122,7 +122,7 @@ public void createOwnerShops(Integer ownerId, OwnerShopsRequest ownerShopsReques .shopCategory(shopCategory) .shop(savedShop) .build(); - shopCategoryMapRepository.save(shopCategoryMap); + savedShop.getShopCategories().add(shopCategoryMap); } } @@ -188,14 +188,14 @@ public void createMenu(Integer shopId, Integer ownerId, CreateMenuRequest create .menuCategory(menuCategory) .menu(savedMenu) .build(); - menuCategoryMapRepository.save(menuCategoryMap); + savedMenu.getMenuCategoryMaps().add(menuCategoryMap); } for (String imageUrl : createMenuRequest.imageUrls()) { MenuImage menuImage = MenuImage.builder() .imageUrl(imageUrl) .menu(savedMenu) .build(); - menuImageRepository.save(menuImage); + savedMenu.getMenuImages().add(menuImage); } if (createMenuRequest.optionPrices() == null) { MenuOption menuOption = MenuOption.builder() @@ -203,7 +203,7 @@ public void createMenu(Integer shopId, Integer ownerId, CreateMenuRequest create .price(createMenuRequest.singlePrice()) .menu(menu) .build(); - menuDetailRepository.save(menuOption); + savedMenu.getMenuOptions().add(menuOption); } else { for (var option : createMenuRequest.optionPrices()) { MenuOption menuOption = MenuOption.builder() @@ -211,7 +211,7 @@ public void createMenu(Integer shopId, Integer ownerId, CreateMenuRequest create .price(option.price()) .menu(menu) .build(); - menuDetailRepository.save(menuOption); + savedMenu.getMenuOptions().add(menuOption); } } } diff --git a/src/main/java/in/koreatech/koin/domain/shop/dto/CreateMenuRequest.java b/src/main/java/in/koreatech/koin/domain/shop/dto/CreateMenuRequest.java index c071cd3d4..81b887387 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/dto/CreateMenuRequest.java +++ b/src/main/java/in/koreatech/koin/domain/shop/dto/CreateMenuRequest.java @@ -37,7 +37,8 @@ public record CreateMenuRequest( """, description = "이미지 URL 리스트", requiredMode = REQUIRED) @Size(max = 3, message = "이미지는 최대 3개까지 입력 가능합니다.") @UniqueUrl(message = "이미지 URL은 중복될 수 없습니다.") - @NotBlankElement(message = "이미지 URL은 필수입니다.") + @NotNull(message = "이미지 URL은 null일 수 없습니다.") + @NotBlankElement(message = "빈 요소가 존재할 수 없습니다.") List imageUrls, @Schema(example = "true", description = "단일 메뉴 여부", requiredMode = REQUIRED) diff --git a/src/main/java/in/koreatech/koin/domain/shop/dto/CreateReviewRequest.java b/src/main/java/in/koreatech/koin/domain/shop/dto/CreateReviewRequest.java index 41e9333bf..c4c906276 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/dto/CreateReviewRequest.java +++ b/src/main/java/in/koreatech/koin/domain/shop/dto/CreateReviewRequest.java @@ -37,7 +37,7 @@ public record CreateReviewRequest( List imageUrls, @Schema(example = "[\"치킨\", \"피자\"]", description = "메뉴 이름", requiredMode = REQUIRED) - @NotBlankElement(message = "빈 요소가 존재합니다.") + @NotBlankElement(message = "빈 요소가 존재할 수 없습니다.") List menuNames ) { @JsonCreator diff --git a/src/main/java/in/koreatech/koin/domain/shop/dto/ModifyMenuRequest.java b/src/main/java/in/koreatech/koin/domain/shop/dto/ModifyMenuRequest.java index 4893f0b1f..0c7ef65ee 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/dto/ModifyMenuRequest.java +++ b/src/main/java/in/koreatech/koin/domain/shop/dto/ModifyMenuRequest.java @@ -36,7 +36,8 @@ public record ModifyMenuRequest( """, description = "이미지 URL 리스트", requiredMode = NOT_REQUIRED) @Size(max = 3, message = "이미지는 최대 3개까지 입력 가능합니다.") @UniqueUrl(message = "이미지 URL은 중복될 수 없습니다.") - @NotBlankElement(message = "이미지 URL은 필수입니다.") + @NotNull(message = "이미지 URL은 null일 수 없습니다.") + @NotBlankElement(message = "빈 요소가 존재할 수 없습니다.") List imageUrls, @Schema(example = "true", description = "단일 메뉴 여부", requiredMode = REQUIRED) diff --git a/src/main/java/in/koreatech/koin/domain/shop/dto/ModifyReviewRequest.java b/src/main/java/in/koreatech/koin/domain/shop/dto/ModifyReviewRequest.java index 8636d7579..52db89735 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/dto/ModifyReviewRequest.java +++ b/src/main/java/in/koreatech/koin/domain/shop/dto/ModifyReviewRequest.java @@ -37,7 +37,7 @@ public record ModifyReviewRequest( List imageUrls, @Schema(example = "[\"치킨\", \"피자\"]", description = "메뉴 이름", requiredMode = REQUIRED) - @NotBlankElement(message = "빈 요소가 존재합니다.") + @NotBlankElement(message = "빈 요소가 존재할 수 없습니다.") List menuNames ) { @JsonCreator diff --git a/src/main/java/in/koreatech/koin/domain/shop/dto/ModifyShopRequest.java b/src/main/java/in/koreatech/koin/domain/shop/dto/ModifyShopRequest.java index 49b6ed232..1c82fbfa0 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/dto/ModifyShopRequest.java +++ b/src/main/java/in/koreatech/koin/domain/shop/dto/ModifyShopRequest.java @@ -51,7 +51,8 @@ public record ModifyShopRequest( [ "https://static.koreatech.in/example.png" ] """, requiredMode = NOT_REQUIRED) @UniqueUrl(message = "이미지 URL은 중복될 수 없습니다.") - @NotBlankElement(message = "빈 요소가 존재합니다.") + @NotNull(message = "이미지 URL은 null일 수 없습니다.") + @NotBlankElement(message = "빈 요소가 존재할 수 없습니다.") List imageUrls, @Schema(example = "수신반점", description = "이름", requiredMode = REQUIRED) diff --git a/src/main/java/in/koreatech/koin/domain/shop/repository/MenuRepository.java b/src/main/java/in/koreatech/koin/domain/shop/repository/MenuRepository.java index ad68626fe..221f1365f 100644 --- a/src/main/java/in/koreatech/koin/domain/shop/repository/MenuRepository.java +++ b/src/main/java/in/koreatech/koin/domain/shop/repository/MenuRepository.java @@ -21,4 +21,6 @@ default Menu getById(Integer menuId) { } List findAllByShopId(Integer shopId); + + List findAll(); } diff --git a/src/main/java/in/koreatech/koin/domain/user/model/User.java b/src/main/java/in/koreatech/koin/domain/user/model/User.java index 90685e4db..6632f4b20 100644 --- a/src/main/java/in/koreatech/koin/domain/user/model/User.java +++ b/src/main/java/in/koreatech/koin/domain/user/model/User.java @@ -46,7 +46,7 @@ public class User extends BaseEntity { private String nickname; @Size(max = 50) - @Column(name = "name", length = 50, unique = true) + @Column(name = "name", length = 50) private String name; @Size(max = 20) diff --git a/src/main/java/in/koreatech/koin/domain/user/repository/UserRepository.java b/src/main/java/in/koreatech/koin/domain/user/repository/UserRepository.java index 30353702a..0a909e9d4 100644 --- a/src/main/java/in/koreatech/koin/domain/user/repository/UserRepository.java +++ b/src/main/java/in/koreatech/koin/domain/user/repository/UserRepository.java @@ -62,4 +62,6 @@ default User getByResetToken(String resetToken) { List findAllByDeviceTokenIsNotNull(); Optional findByPhoneNumber(String phoneNumber); + + List findAllByName(String name); } diff --git a/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java b/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java index 4ca069153..cd8ad0655 100644 --- a/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java +++ b/src/main/java/in/koreatech/koin/domain/user/service/StudentService.java @@ -120,7 +120,6 @@ public ModelAndView authenticate(AuthTokenRequest request) { } Student student = studentTemporaryStatus.get().toStudent(passwordEncoder); - studentRepository.save(student); userRepository.save(student.getUser()); diff --git a/src/main/java/in/koreatech/koin/global/config/RedisConfig.java b/src/main/java/in/koreatech/koin/global/config/RedisConfig.java index ab2f859b4..9c701cb02 100644 --- a/src/main/java/in/koreatech/koin/global/config/RedisConfig.java +++ b/src/main/java/in/koreatech/koin/global/config/RedisConfig.java @@ -13,6 +13,7 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.http.client.BufferingClientHttpRequestFactory; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.http.converter.StringHttpMessageConverter; @@ -32,6 +33,7 @@ public RedisTemplate redisTemplate(RedisConnectionFactory connec ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(mapper); + template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(serializer); template.setConnectionFactory(connectionFactory); return template; diff --git a/src/main/java/in/koreatech/koin/global/config/WebConfig.java b/src/main/java/in/koreatech/koin/global/config/WebConfig.java index 84ebd24e5..909de47e4 100644 --- a/src/main/java/in/koreatech/koin/global/config/WebConfig.java +++ b/src/main/java/in/koreatech/koin/global/config/WebConfig.java @@ -21,6 +21,7 @@ import in.koreatech.koin.global.host.ServerURLInterceptor; import in.koreatech.koin.global.ipaddress.IpAddressArgumentResolver; import in.koreatech.koin.global.ipaddress.IpAddressInterceptor; +import in.koreatech.koin.global.useragent.UserAgentArgumentResolver; import lombok.RequiredArgsConstructor; @Configuration @@ -33,6 +34,7 @@ public class WebConfig implements WebMvcConfigurer { private final AuthArgumentResolver authArgumentResolver; private final IpAddressInterceptor ipAddressInterceptor; private final ServerURLArgumentResolver serverURLArgumentResolver; + private final UserAgentArgumentResolver userAgentArgumentResolver; private final ServerURLInterceptor serverURLInterceptor; private final CorsProperties corsProperties; @@ -55,6 +57,7 @@ public void addArgumentResolvers(List resolvers) resolvers.add(ipAddressArgumentResolver); resolvers.add(userIdArgumentResolver); resolvers.add(serverURLArgumentResolver); + resolvers.add(userAgentArgumentResolver); } @Override diff --git a/src/main/java/in/koreatech/koin/global/domain/slack/model/SlackNotificationFactory.java b/src/main/java/in/koreatech/koin/global/domain/slack/model/SlackNotificationFactory.java index 072cfe4c1..4ed4c52d0 100644 --- a/src/main/java/in/koreatech/koin/global/domain/slack/model/SlackNotificationFactory.java +++ b/src/main/java/in/koreatech/koin/global/domain/slack/model/SlackNotificationFactory.java @@ -14,17 +14,20 @@ public class SlackNotificationFactory { private final String adminReviewPageUrl; private final String ownerEventNotificationUrl; private final String eventNotificationUrl; + private final String reviewNotificationUrl; public SlackNotificationFactory( @Value("${koin.admin.shop.url}") String adminPageUrl, @Value("${koin.admin.review.url}") String adminReviewPageUrl, @Value("${slack.koin_event_notify_url}") String eventNotificationUrl, - @Value("${slack.koin_owner_event_notify_url}") String ownerEventNotificationUrl + @Value("${slack.koin_owner_event_notify_url}") String ownerEventNotificationUrl, + @Value("${slack.koin_shop_review_notify_url}") String reviewNotificationUrl ) { this.adminPageUrl = adminPageUrl; this.adminReviewPageUrl = adminReviewPageUrl; this.eventNotificationUrl = eventNotificationUrl; this.ownerEventNotificationUrl = ownerEventNotificationUrl; + this.reviewNotificationUrl = reviewNotificationUrl; } /** @@ -149,7 +152,7 @@ public SlackNotification generateReviewRegisterNotification( Integer rating ) { return SlackNotification.builder() - .slackUrl(eventNotificationUrl) + .slackUrl(reviewNotificationUrl) .text(String.format(""" `%s에 새로운 리뷰가 등록되었습니다.` 내용: `%s` @@ -174,7 +177,7 @@ public SlackNotification generateReviewReportNotification( String shop ) { return SlackNotification.builder() - .slackUrl(eventNotificationUrl) + .slackUrl(reviewNotificationUrl) .text(String.format(""" `%s의 리뷰가 신고되었습니다.` `신고를 처리해주세요!!` diff --git a/src/main/java/in/koreatech/koin/global/exception/ErrorResponse.java b/src/main/java/in/koreatech/koin/global/exception/ErrorResponse.java index d6c8cbae0..91c4ed5a7 100644 --- a/src/main/java/in/koreatech/koin/global/exception/ErrorResponse.java +++ b/src/main/java/in/koreatech/koin/global/exception/ErrorResponse.java @@ -11,10 +11,12 @@ public class ErrorResponse { private final int status; private final String message; private final String code; + private final String errorTraceId; - public ErrorResponse(int status, String message) { + public ErrorResponse(int status, String message, String errorTraceId) { this.status = status; this.message = message; this.code = ""; + this.errorTraceId = errorTraceId; } } diff --git a/src/main/java/in/koreatech/koin/global/exception/GlobalExceptionHandler.java b/src/main/java/in/koreatech/koin/global/exception/GlobalExceptionHandler.java index 1524cf59b..71b5a9505 100644 --- a/src/main/java/in/koreatech/koin/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/in/koreatech/koin/global/exception/GlobalExceptionHandler.java @@ -4,6 +4,7 @@ import java.util.Enumeration; import java.util.HashMap; import java.util.Map; +import java.util.UUID; import java.util.stream.Collectors; import org.apache.catalina.connector.ClientAbortException; @@ -230,7 +231,9 @@ private ResponseEntity buildErrorResponse( HttpStatus httpStatus, String message ) { - var response = new ErrorResponse(httpStatus.value(), message); + String errorTraceId = UUID.randomUUID().toString(); + log.warn("traceId: {}", errorTraceId); + var response = new ErrorResponse(httpStatus.value(), message, errorTraceId); return ResponseEntity.status(httpStatus).body(response); } diff --git a/src/main/java/in/koreatech/koin/global/useragent/UserAgent.java b/src/main/java/in/koreatech/koin/global/useragent/UserAgent.java new file mode 100644 index 000000000..24765a0a2 --- /dev/null +++ b/src/main/java/in/koreatech/koin/global/useragent/UserAgent.java @@ -0,0 +1,14 @@ +package in.koreatech.koin.global.useragent; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.swagger.v3.oas.annotations.Parameter; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Parameter(hidden = true) +public @interface UserAgent { +} diff --git a/src/main/java/in/koreatech/koin/global/useragent/UserAgentArgumentResolver.java b/src/main/java/in/koreatech/koin/global/useragent/UserAgentArgumentResolver.java new file mode 100644 index 000000000..bf96de278 --- /dev/null +++ b/src/main/java/in/koreatech/koin/global/useragent/UserAgentArgumentResolver.java @@ -0,0 +1,65 @@ +package in.koreatech.koin.global.useragent; + +import java.io.IOException; + +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import ua_parser.Client; +import ua_parser.Parser; + +@Component +public class UserAgentArgumentResolver implements HandlerMethodArgumentResolver { + + private final Parser uaParser = new Parser(); + + public UserAgentArgumentResolver() throws IOException { + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(UserAgent.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + String userAgent = webRequest.getHeader("User-Agent"); + if (userAgent == null) { + return null; + } + + Client client = uaParser.parse(userAgent); + String type = determineDeviceType(userAgent); + return UserAgentInfo.builder() + .model(client.device.family) + .type(type) + .build(); + } + + + private String determineDeviceType(String userAgent) { + // 태블릿 기기를 나타내는 패턴 검사 + String[] tabletIndicators = {"Tablet", "iPad"}; + for (String indicator : tabletIndicators) { + if (userAgent.toLowerCase().contains(indicator.toLowerCase())) { + return "Tablet"; + } + } + + // 모바일 기기를 나타내는 패턴 검사 + String[] mobileIndicators = {"Mobile", "Mobi", "Android", "iPhone", "Windows Phone"}; + for (String indicator : mobileIndicators) { + if (userAgent.toLowerCase().contains(indicator.toLowerCase())) { + return "Mobile"; + } + } + + // 모바일과 태블릿 모두 해당하지 않으면 PC로 간주 + return "PC"; + } +} diff --git a/src/main/java/in/koreatech/koin/global/useragent/UserAgentInfo.java b/src/main/java/in/koreatech/koin/global/useragent/UserAgentInfo.java new file mode 100644 index 000000000..d80d094fe --- /dev/null +++ b/src/main/java/in/koreatech/koin/global/useragent/UserAgentInfo.java @@ -0,0 +1,17 @@ +package in.koreatech.koin.global.useragent; + +import lombok.Builder; +import lombok.Getter; + +@Getter +public class UserAgentInfo { + + private String model; + private String type; + + @Builder + private UserAgentInfo(String model, String type) { + this.model = model; + this.type = type; + } +} diff --git a/src/main/resources/db/migration/V55__add_abtest_tables.sql b/src/main/resources/db/migration/V55__add_abtest_tables.sql new file mode 100644 index 000000000..2a63e04d9 --- /dev/null +++ b/src/main/resources/db/migration/V55__add_abtest_tables.sql @@ -0,0 +1,61 @@ +CREATE TABLE `device` +( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `user_id` int unsigned NOT NULL, + `model` varchar(100) DEFAULT NULL, + `type` varchar(100) DEFAULT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +); + +CREATE TABLE `access_history` +( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `device_id` int unsigned DEFAULT NULL, + `last_accessed_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +); + +CREATE TABLE `abtest_variable` +( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `abtest_id` int unsigned NOT NULL, + `name` varchar(255) NOT NULL, + `display_name` varchar(255) NOT NULL, + `rate` int unsigned NOT NULL, + `count` int NOT NULL DEFAULT '0', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +); + +CREATE TABLE `access_history_abtest_variable` +( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `access_history_id` int unsigned NOT NULL, + `variable_id` int unsigned NOT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +); + +CREATE TABLE `abtest` +( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `title` varchar(255) NOT NULL, + `display_title` varchar(255) NOT NULL, + `description` varchar(255) DEFAULT NULL, + `creator` varchar(50) DEFAULT NULL, + `team` varchar(50) DEFAULT NULL, + `winner_id` int unsigned DEFAULT NULL, + `status` varchar(50) NOT NULL DEFAULT 'IN_PROGRESS', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + constraint title_UNIQUE + unique (`title`) +); + diff --git a/src/main/resources/db/migration/V56__add_abtest_tables_fk.sql b/src/main/resources/db/migration/V56__add_abtest_tables_fk.sql new file mode 100644 index 000000000..2f59c908f --- /dev/null +++ b/src/main/resources/db/migration/V56__add_abtest_tables_fk.sql @@ -0,0 +1,58 @@ +ALTER TABLE `koin`.`device` + ADD INDEX `FK_DEVICE_ON_USER_ID_idx` (`user_id` ASC) VISIBLE; +; +ALTER TABLE `koin`.`device` + ADD CONSTRAINT `FK_DEVICE_ON_USER_ID` + FOREIGN KEY (`user_id`) + REFERENCES `koin`.`users` (`id`) + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +ALTER TABLE `koin`.`access_history` + ADD INDEX `FK_ACCESS_HISTORY_ON_DEVICE_ID_idx` (`device_id` ASC) VISIBLE; + +ALTER TABLE `koin`.`access_history` + ADD CONSTRAINT `FK_ACCESS_HISTORY_ON_DEVICE_ID` + FOREIGN KEY (`device_id`) + REFERENCES `koin`.`device` (`id`) + ON DELETE NO ACTION + ON UPDATE NO ACTION; + + +ALTER TABLE `koin`.`access_history_abtest_variable` + ADD INDEX `FK_ACCESS_HISTORY_ABTEST_VARIABLE_ON_ACCESS_HISTORY_ID_idx` (`access_history_id` ASC) VISIBLE, +ADD INDEX `FK_ACCESS_HISTORY_ABTEST_VARIABLE_ON_ACCESS_VARIABLE_ID_idx` (`variable_id` ASC) VISIBLE; +; +ALTER TABLE `koin`.`access_history_abtest_variable` + ADD CONSTRAINT `FK_ACCESS_HISTORY_ABTEST_VARIABLE_ON_ACCESS_HISTORY_ID` + FOREIGN KEY (`access_history_id`) + REFERENCES `koin`.`access_history` (`id`) + ON DELETE NO ACTION + ON UPDATE NO ACTION, +ADD CONSTRAINT `FK_ACCESS_HISTORY_ABTEST_VARIABLE_ON_ACCESS_VARIABLE_ID` + FOREIGN KEY (`variable_id`) + REFERENCES `koin`.`abtest_variable` (`id`) + ON DELETE NO ACTION + ON UPDATE NO ACTION; + + +ALTER TABLE `koin`.`abtest_variable` + ADD INDEX `FK_ABTEST_VARIABLE_ON_ABTEST_ID_idx` (`abtest_id` ASC) VISIBLE; +; +ALTER TABLE `koin`.`abtest_variable` + ADD CONSTRAINT `FK_ABTEST_VARIABLE_ON_ABTEST_ID` + FOREIGN KEY (`abtest_id`) + REFERENCES `koin`.`abtest` (`id`) + ON DELETE NO ACTION + ON UPDATE NO ACTION; + + +ALTER TABLE `koin`.`abtest` + ADD INDEX `FK_ABTEST_ON_WINNER_ID_idx` (`winner_id` ASC) VISIBLE; +; +ALTER TABLE `koin`.`abtest` + ADD CONSTRAINT `FK_ABTEST_ON_WINNER_ID` + FOREIGN KEY (`winner_id`) + REFERENCES `koin`.`abtest_variable` (`id`) + ON DELETE NO ACTION + ON UPDATE NO ACTION; diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index 7e0f751d9..e396b4fe7 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -25,6 +25,11 @@ + + logs/app.log + + + ${consoleLogPattern} @@ -47,66 +52,8 @@ - - - - - - - - - - - ${infoLogPath}/info.log - - 7 - 1GB - 2GB - ${infoLogPath}/${fileNamePattern} - - - ${defaultLogPattern} - - - INFO - ACCEPT - NEUTRAL - - - WARN - ACCEPT - NEUTRAL - - - ERROR - ACCEPT - DENY - - - - - ${errorLogPath}/error.log - - 7 - 1GB - 2GB - ${errorLogPath}/${fileNamePattern} - - - ${defaultLogPattern} - - - ERROR - ACCEPT - DENY - - - - - - + diff --git a/src/test/java/in/koreatech/koin/AcceptanceTest.java b/src/test/java/in/koreatech/koin/AcceptanceTest.java index 0be2b62d4..e9c5011ec 100644 --- a/src/test/java/in/koreatech/koin/AcceptanceTest.java +++ b/src/test/java/in/koreatech/koin/AcceptanceTest.java @@ -1,19 +1,19 @@ package in.koreatech.koin; -import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; - import java.time.Clock; import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.SpyBean; -import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.context.annotation.Import; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.context.transaction.TestTransaction; +import org.springframework.test.web.servlet.MockMvc; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.MySQLContainer; import org.testcontainers.junit.jupiter.Container; @@ -28,12 +28,12 @@ import in.koreatech.koin.domain.shop.model.ReviewEventListener; import in.koreatech.koin.domain.shop.model.ShopEventListener; import in.koreatech.koin.domain.user.model.StudentEventListener; -import in.koreatech.koin.util.TestCircuitBreakerClient; import in.koreatech.koin.support.DBInitializer; -import io.restassured.RestAssured; +import in.koreatech.koin.util.TestCircuitBreakerClient; import jakarta.persistence.EntityManager; -@SpringBootTest(webEnvironment = RANDOM_PORT) +@SpringBootTest +@AutoConfigureMockMvc @Import({DBInitializer.class, TestJpaConfiguration.class, TestTimeConfig.class, TestRedisConfiguration.class}) @ActiveProfiles("test") public abstract class AcceptanceTest { @@ -41,8 +41,8 @@ public abstract class AcceptanceTest { private static final String ROOT = "test"; private static final String ROOT_PASSWORD = "1234"; - @LocalServerPort - protected int port; + @Autowired + public MockMvc mockMvc; @MockBean protected OwnerEventListener ownerEventListener; @@ -97,18 +97,18 @@ private static void configureProperties(final DynamicPropertyRegistry registry) static { mySqlContainer = (MySQLContainer)new MySQLContainer("mysql:8.0.29") - .withDatabaseName("test") - .withUsername(ROOT) - .withPassword(ROOT_PASSWORD) - .withCommand("--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci"); + .withDatabaseName("test") + .withUsername(ROOT) + .withPassword(ROOT_PASSWORD) + .withCommand("--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci"); redisContainer = new GenericContainer<>( - DockerImageName.parse("redis:7.0.9")) - .withExposedPorts(6379); + DockerImageName.parse("redis:7.0.9")) + .withExposedPorts(6379); mongoContainer = new GenericContainer<>( - DockerImageName.parse("mongo:6.0.14")) - .withExposedPorts(27017); + DockerImageName.parse("mongo:6.0.14")) + .withExposedPorts(27017); mySqlContainer.start(); redisContainer.start(); @@ -116,10 +116,18 @@ private static void configureProperties(final DynamicPropertyRegistry registry) } @BeforeEach - void delete() { - if (RestAssured.port == RestAssured.UNDEFINED_PORT) { - RestAssured.port = port; - } + void initIncrement() { + dataInitializer.initIncrement(); + dataInitializer.clearRedis(); + } + + protected void clear() { dataInitializer.clear(); } + + protected void forceVerify(Runnable runnable) { + TestTransaction.flagForCommit(); + TestTransaction.end(); + runnable.run(); + } } diff --git a/src/test/java/in/koreatech/koin/acceptance/AbtestApiTest.java b/src/test/java/in/koreatech/koin/acceptance/AbtestApiTest.java new file mode 100644 index 000000000..1f59994f4 --- /dev/null +++ b/src/test/java/in/koreatech/koin/acceptance/AbtestApiTest.java @@ -0,0 +1,543 @@ +package in.koreatech.koin.acceptance; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.Optional; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; + +import in.koreatech.koin.AcceptanceTest; +import in.koreatech.koin.admin.abtest.model.Abtest; +import in.koreatech.koin.admin.abtest.model.AbtestVariable; +import in.koreatech.koin.admin.abtest.model.AccessHistoryAbtestVariable; +import in.koreatech.koin.admin.abtest.model.Device; +import in.koreatech.koin.admin.abtest.repository.AbtestRepository; +import in.koreatech.koin.admin.abtest.repository.DeviceRepository; +import in.koreatech.koin.domain.owner.model.Owner; +import in.koreatech.koin.domain.user.model.Student; +import in.koreatech.koin.domain.user.model.User; +import in.koreatech.koin.fixture.AbtestFixture; +import in.koreatech.koin.fixture.DeviceFixture; +import in.koreatech.koin.fixture.UserFixture; + +@Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class AbtestApiTest extends AcceptanceTest { + + @Autowired + private TransactionTemplate transactionTemplate; + + @Autowired + private AbtestFixture abtestFixture; + + @Autowired + private UserFixture userFixture; + + @Autowired + private DeviceFixture deviceFixture; + + @Autowired + private AbtestRepository abtestRepository; + + @Autowired + private DeviceRepository deviceRepository; + + private User admin; + private String adminToken; + + @BeforeAll + void setUp() { + clear(); + admin = userFixture.코인_운영자(); + adminToken = userFixture.getToken(admin); + } + + @Test + void 실험을_생성한다() throws Exception { + mockMvc.perform( + post("/abtest") + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + adminToken) + .content(""" + { + "display_title": "사장님 전화번호 회원가입 실험", + "creator": "송선권", + "team": "campus", + "description": "세부설명", + "status": "IN_PROGRESS", + "title": "business.register.phone_number", + "variables": [ + { + "rate": 33, + "display_name": "실험군 A", + "name": "A" + }, + { + "rate": 33, + "display_name": "실험군 B", + "name": "B" + }, + { + "rate": 34, + "display_name": "실험군 C", + "name": "C" + } + ] + } + """) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { + "id": 1, + "display_title": "사장님 전화번호 회원가입 실험", + "creator": "송선권", + "team": "campus", + "description": "세부설명", + "title": "business.register.phone_number", + "status": "IN_PROGRESS", + "winner_name": null, + "variables": [ + { + "rate": 33, + "display_name": "실험군 A", + "name": "A" + }, + { + "rate": 33, + "display_name": "실험군 B", + "name": "B" + }, + { + "rate": 34, + "display_name": "실험군 C", + "name": "C" + } + ], + "created_at": "2024-01-15 12:00:00", + "updated_at": "2024-01-15 12:00:00" + } + """)); + } + + @Test + void 실험을_단건_조회한다() throws Exception { + Abtest abtest = abtestFixture.식단_UI_실험(); + + mockMvc.perform( + get("/abtest/{id}", abtest.getId()) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + adminToken) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { + "id": 1, + "display_title": "식단_UI_실험", + "creator": "송선권", + "team": "campus", + "description": "세부설명", + "title": "dining_ui_test", + "status": "IN_PROGRESS", + "winner_name": null, + "variables": [ + { + "rate": 50, + "display_name": "실험군 A", + "name": "A" + }, + { + "rate": 50, + "display_name": "실험군 B", + "name": "B" + } + ], + "created_at": "2024-01-15 12:00:00", + "updated_at": "2024-01-15 12:00:00" + } + """)); + } + + @Test + void 실험_목록을_조회한다() throws Exception { + Abtest abtest1 = abtestFixture.식단_UI_실험(); + Abtest abtest2 = abtestFixture.주변상점_UI_실험(); + + mockMvc.perform( + get("/abtest") + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + adminToken) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { + "total_count": 2, + "current_count": 2, + "total_page": 1, + "current_page": 1, + "tests": [ + { + "id": 2, + "status": "IN_PROGRESS", + "creator": "송선권", + "team": "campus", + "display_title": "주변상점_UI_실험", + "title": "shop_ui_test", + "winner_name": null, + "created_at": "2024-01-15 12:00:00", + "updated_at": "2024-01-15 12:00:00" + }, + { + "id": 1, + "status": "IN_PROGRESS", + "creator": "송선권", + "team": "campus", + "display_title": "식단_UI_실험", + "title": "dining_ui_test", + "winner_name": null, + "created_at": "2024-01-15 12:00:00", + "updated_at": "2024-01-15 12:00:00" + } + ] + } + """)); + } + + @Test + void 실험_목록을_조회한다_페이지네이션() throws Exception { + for (int i = 0; i < 10; i++) { + abtestFixture.식단_UI_실험(i); + } + + mockMvc.perform( + get("/abtest?page=2&limit=8") + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + adminToken) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { + "total_count": 10, + "current_count": 2, + "total_page": 2, + "current_page": 2, + "tests": [ + { + "id": 2, + "status": "IN_PROGRESS", + "creator": "송선권", + "team": "campus", + "display_title": "식단_UI_실험", + "title": "dining_ui_test1", + "winner_name": null, + "created_at": "2024-01-15 12:00:00", + "updated_at": "2024-01-15 12:00:00" + }, + { + "id": 1, + "status": "IN_PROGRESS", + "creator": "송선권", + "team": "campus", + "display_title": "식단_UI_실험", + "title": "dining_ui_test0", + "winner_name": null, + "created_at": "2024-01-15 12:00:00", + "updated_at": "2024-01-15 12:00:00" + } + ] + } + """)); + } + + @Test + void 실험을_수정한다() throws Exception { + Abtest abtest = abtestFixture.식단_UI_실험(); + + mockMvc.perform( + put("/abtest/{id}", abtest.getId()) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + adminToken) + .content(""" + { + "display_title": "식단_UI_실험", + "creator": "김성재", + "team": "user", + "title": "dining_ui_test", + "description": "세부설명2", + "variables": [ + { + "rate": 10, + "display_name": "실험군 A", + "name": "A" + }, + { + "rate": 90, + "display_name": "실험군 B", + "name": "B" + } + ], + "created_at": "2024-01-15 12:00:00", + "updated_at": "2024-01-15 12:00:00" + } + """) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { + "id": 1, + "display_title": "식단_UI_실험", + "creator": "김성재", + "team": "user", + "title": "dining_ui_test", + "status": "IN_PROGRESS", + "description": "세부설명2", + "winner_name": null, + "variables": [ + { + "rate": 10, + "display_name": "실험군 A", + "name": "A" + }, + { + "rate": 90, + "display_name": "실험군 B", + "name": "B" + } + ], + "created_at": "2024-01-15 12:00:00", + "updated_at": "2024-01-15 12:00:00" + } + """)); + } + + @Test + void 실험을_삭제한다() throws Exception { + Abtest abtest = abtestFixture.식단_UI_실험(); + + mockMvc.perform( + delete("/abtest/{id}", abtest.getId()) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + adminToken) + ) + .andExpect(status().isNoContent()); + + assertThat(abtestRepository.findById(abtest.getId())).isNotPresent(); + } + + @Test + void 실험을_종료한다() throws Exception { + final Abtest abtest = abtestFixture.식단_UI_실험(); + String winner = "A"; + + mockMvc.perform( + post("/abtest/close/{id}", abtest.getId()) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + adminToken) + .content(String.format(""" + { + "winner_name": "%s" + } + """, winner)) + ) + .andExpect(status().isOk()); + + transactionTemplate.executeWithoutResult(status -> { + Abtest result = abtestRepository.getById(abtest.getId()); + assertSoftly( + softly -> { + softly.assertThat(result.getStatus().name()).isEqualTo("CLOSED"); + softly.assertThat(result.getWinner().getName()).isEqualTo(winner); + } + ); + }); + } + + @Test + void 실험군_수동편입_이름으로_유저_목록을_조회한다() throws Exception { + Student student = userFixture.성빈_학생(); + Owner owner = userFixture.성빈_사장님(); + + mockMvc.perform( + get("/abtest/user") + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + adminToken) + .param("name", student.getUser().getName()) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { + "users": [ + { + "id": 2, + "name" : "박성빈", + "detail": "testsungbeen@koreatech.ac.kr" + }, + { + "id": 3, + "name" : "박성빈", + "detail": "01098765439" + } + ] + } + """)); + } + + @Test + void 실험군_수동편입_유저_ID로_기기_목록을_조회한다() throws Exception { + Student student = userFixture.성빈_학생(); + Device device1 = deviceFixture.아이폰(student.getUser().getId()); + Device device2 = deviceFixture.갤럭시(student.getUser().getId()); + + mockMvc.perform( + get("/abtest/user/{userId}/device", student.getUser().getId()) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + adminToken) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { + "devices": [ + { + "id": 1, + "type": "mobile", + "model" : "아이폰14", + "last_accessed_at": "2024-01-15" + }, + { + "id": 2, + "type": "mobile", + "model" : "갤럭시24", + "last_accessed_at": "2024-01-15" + } + ] + } + """)); + } + + @Test + void 특정_유저의_실험군을_수동으로_편입시킨다() throws Exception { + Student student = userFixture.성빈_학생(); + Device device = deviceFixture.아이폰(student.getUser().getId()); + Abtest abtest = abtestFixture.식단_UI_실험(); + + mockMvc.perform( + post("/abtest/{id}/move", abtest.getId()) + .contentType(MediaType.APPLICATION_JSON) + .header("Authorization", "Bearer " + adminToken) + .content(String.format(""" + { + "device_id": %d, + "variable_name": "A" + } + """, device.getId())) + ) + .andExpect(status().isOk()); + + transactionTemplate.executeWithoutResult(status -> { + assertSoftly( + softly -> { + Device result = deviceRepository.getById(device.getId()); + Optional variable = result.getAccessHistory() + .getAccessHistoryAbtestVariables() + .stream() + .map(AccessHistoryAbtestVariable::getVariable) + .filter(var -> var.getAbtest().getTitle().equals(abtest.getTitle())) + .findAny(); + softly.assertThat(variable.get().getName()).isEqualTo("A"); + } + ); + }); + } + + @Test + void 자신의_실험군을_조회한다() throws Exception { + Student student = userFixture.성빈_학생(); + final Device device = deviceFixture.아이폰(student.getUser().getId()); + Abtest abtest = abtestFixture.식단_UI_실험(); + + mockMvc.perform( + post("/abtest/assign") + .contentType(MediaType.APPLICATION_JSON) + .header("access_history_id", device.getAccessHistory().getId()) + .content(String.format(""" + { + "title": "dining_ui_test" + } + """)) + ); + + MvcResult mvcResult = mockMvc.perform( + get("/abtest/me") + .contentType(MediaType.APPLICATION_JSON) + .header("access_history_id", device.getAccessHistory().getId()) + .param("title", abtest.getTitle()) + ) + .andExpect(status().isOk()) + .andReturn(); + String responseBody = mvcResult.getResponse().getContentAsString(); + + transactionTemplate.executeWithoutResult(status -> { + assertSoftly( + softly -> { + Device result = deviceRepository.getById(device.getId()); + Optional variable = result.getAccessHistory() + .getAccessHistoryAbtestVariables() + .stream() + .map(AccessHistoryAbtestVariable::getVariable) + .filter(var -> var.getAbtest().getTitle().equals(abtest.getTitle())) + .findAny(); + softly.assertThat(responseBody).isEqualTo(variable.get().getName()); + } + ); + }); + } + + @Test + void 실험군_자동_편입_실험군에_최초로_편입된다() throws Exception { + Student student = userFixture.성빈_학생(); + Device device1 = deviceFixture.아이폰(student.getUser().getId()); + Device device2 = deviceFixture.갤럭시(student.getUser().getId()); + Abtest abtest = abtestFixture.식단_UI_실험(); + + MvcResult mvcResult = mockMvc.perform( + post("/abtest/assign") + .contentType(MediaType.APPLICATION_JSON) + .header("access_history_id", device1.getAccessHistory().getId()) + .content(String.format(""" + { + "title": "dining_ui_test" + } + """)) + ) + .andExpect(status().isOk()) + .andReturn(); + String responseBody = mvcResult.getResponse().getContentAsString(); + + MvcResult mvcResult2 = mockMvc.perform( + post("/abtest/assign") + .contentType(MediaType.APPLICATION_JSON) + .header("access_history_id", device2.getAccessHistory().getId()) + .content(String.format(""" + { + "title": "dining_ui_test" + } + """)) + ) + .andExpect(status().isOk()) + .andReturn(); + String responseBody2 = mvcResult2.getResponse().getContentAsString(); + assertNotEquals(responseBody, responseBody2); + } +} diff --git a/src/test/java/in/koreatech/koin/acceptance/ActivityApiTest.java b/src/test/java/in/koreatech/koin/acceptance/ActivityApiTest.java index 1d372c434..c09f806bf 100644 --- a/src/test/java/in/koreatech/koin/acceptance/ActivityApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/ActivityApiTest.java @@ -1,26 +1,30 @@ package in.koreatech.koin.acceptance; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import java.time.LocalDate; -import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.transaction.annotation.Transactional; import in.koreatech.koin.AcceptanceTest; import in.koreatech.koin.fixture.ActivityFixture; -import in.koreatech.koin.support.JsonAssertions; -import io.restassured.RestAssured; @SuppressWarnings("NonAsciiCharacters") +@Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class ActivityApiTest extends AcceptanceTest { @Autowired protected ActivityFixture activityFixture; @Test - @DisplayName("BCSD Lab 활동 내역을 조회한다.") - void getActivities() { + void BCSDLab_활동_내역을_조회한다() throws Exception { activityFixture.builder() .title("BCSD/KAP 통합") .description("BCSD와 KAP가 통합되었습니다.") @@ -48,17 +52,13 @@ void getActivities() { .isDeleted(false) .build(); - var response = RestAssured - .given() - .when() - .param("year", 2019) - .get("/activities") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/activities") + .param("year", "2019") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "Activities": [ { @@ -88,12 +88,11 @@ void getActivities() { } ] } - """); + """)); } @Test - @DisplayName("BCSD Lab 활동 내역을 조회한다. - 파라미터가 없는 경우 전체조회") - void getActivitiesWithoutYear() { + void BCSDLab_활동_내역을_조회한다_파라미터가_없는_경우_전체조회() throws Exception { activityFixture.builder() .title("BCSD/KAP 통합") .description("BCSD와 KAP가 통합되었습니다.") @@ -121,16 +120,12 @@ void getActivitiesWithoutYear() { .isDeleted(false) .build(); - var response = RestAssured - .given() - .when() - .get("/activities") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/activities") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "Activities": [ { @@ -172,6 +167,6 @@ void getActivitiesWithoutYear() { } ] } - """); + """)); } } diff --git a/src/test/java/in/koreatech/koin/acceptance/ArticleApiTest.java b/src/test/java/in/koreatech/koin/acceptance/ArticleApiTest.java new file mode 100644 index 000000000..653e3db92 --- /dev/null +++ b/src/test/java/in/koreatech/koin/acceptance/ArticleApiTest.java @@ -0,0 +1,104 @@ +package in.koreatech.koin.acceptance; + +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.time.LocalDate; +import java.util.List; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.transaction.annotation.Transactional; + +import in.koreatech.koin.AcceptanceTest; +import in.koreatech.koin.domain.community.article.model.Article; +import in.koreatech.koin.domain.community.article.model.Board; +import in.koreatech.koin.domain.community.article.model.Comment; +import in.koreatech.koin.domain.community.article.repository.ArticleRepository; +import in.koreatech.koin.domain.community.article.repository.CommentRepository; +import in.koreatech.koin.domain.user.model.Student; +import in.koreatech.koin.fixture.ArticleFixture; +import in.koreatech.koin.fixture.BoardFixture; +import in.koreatech.koin.fixture.UserFixture; + +@SuppressWarnings("NonAsciiCharacters") +@Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ArticleApiTest extends AcceptanceTest { + + @Autowired + private ArticleRepository articleRepository; + + @Autowired + private CommentRepository commentRepository; + + @Autowired + private UserFixture userFixture; + + @Autowired + private ArticleFixture articleFixture; + + @Autowired + private BoardFixture boardFixture; + + Student student; + Board board; + Article article1, article2; + + @BeforeAll + void givenBeforeEach() { + clear(); + student = userFixture.준호_학생(); + board = boardFixture.자유게시판(); + article1 = articleFixture.자유글_1(board); + article2 = articleFixture.자유글_2(board); + } + + @Test + void 특정_게시글을_단일_조회한다() throws Exception { + // given + Comment request = Comment.builder() + .article(article1) + .content("댓글") + .userId(1) + .nickname("BCSD") + .isDeleted(false) + .build(); + commentRepository.save(request); + + mockMvc.perform( + get("/articles/{articleId}", article1.getId()) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { + "id": 1, + "board_id": 1, + "title": "자유 글의 제목입니다", + "content": "

내용

", + "author": "작성자1", + "hit": 3, + "attachments": [ + { + "id": 1, + "name": "첨부파일1.png", + "url": "https://example.com", + "created_at": "2024-01-15 12:00:00", + "updated_at": "2024-01-15 12:00:00" + } + ], + "registered_at": "2024-01-15", + "prev_id": null, + "next_id": 2, + "updated_at": "2024-01-15 12:00:00" + } + """)); + } +} diff --git a/src/test/java/in/koreatech/koin/acceptance/AuthApiTest.java b/src/test/java/in/koreatech/koin/acceptance/AuthApiTest.java index 1446494e9..7ff962522 100644 --- a/src/test/java/in/koreatech/koin/acceptance/AuthApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/AuthApiTest.java @@ -1,12 +1,19 @@ package in.koreatech.koin.acceptance; import static in.koreatech.koin.domain.user.model.UserType.STUDENT; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.transaction.annotation.Transactional; + +import com.fasterxml.jackson.databind.JsonNode; import in.koreatech.koin.AcceptanceTest; import in.koreatech.koin.domain.user.model.User; @@ -15,10 +22,10 @@ import in.koreatech.koin.domain.user.repository.UserTokenRepository; import in.koreatech.koin.fixture.UserFixture; import in.koreatech.koin.support.JsonAssertions; -import io.restassured.RestAssured; -import io.restassured.http.ContentType; @SuppressWarnings("NonAsciiCharacters") +@Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class AuthApiTest extends AcceptanceTest { @Autowired @@ -31,8 +38,7 @@ class AuthApiTest extends AcceptanceTest { private UserTokenRepository tokenRepository; @Test - @DisplayName("사용자가 로그인을 수행한다") - void userLoginSuccess() { + void 사용자가_로그인을_수행한다() throws Exception { User user = userFixture.builder() .password("1234") .nickname("주노") @@ -44,41 +50,27 @@ void userLoginSuccess() { .isDeleted(false) .build(); - var response = RestAssured - .given() - .body(""" - { - "email": "test@koreatech.ac.kr", - "password": "1234" - } - """) - .contentType(ContentType.JSON) - .when() - .post("/user/login") - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); - - User userResult = userRepository.findById(user.getId()).get(); - UserToken token = tokenRepository.findById(userResult.getId()).get(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" - { - "token": "%s", - "refresh_token": "%s", - "user_type": "%s" - } - """, - response.jsonPath().getString("token"), - token.getRefreshToken(), - user.getUserType().name() - )); + MvcResult result = mockMvc.perform( + post("/user/login") + .content(""" + { + "email": "test@koreatech.ac.kr", + "password": "1234" + } + """ + ) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.token").isNotEmpty()) + .andExpect( + jsonPath("$.refresh_token").value(tokenRepository.findById(user.getId()).get().getRefreshToken())) + .andExpect(jsonPath("$.user_type").value(user.getUserType().name())) + .andReturn(); } @Test - @DisplayName("사용자가 로그인 이후 로그아웃을 수행한다") - void userLogoutSuccessg() { + void 사용자가_로그인_이후_로그아웃을_수행한다() throws Exception { User user = userFixture.builder() .password("1234") .nickname("주노") @@ -90,36 +82,33 @@ void userLogoutSuccessg() { .isDeleted(false) .build(); - var response = RestAssured - .given() - .body(""" - { - "email": "test@koreatech.ac.kr", - "password": "1234" - } - """) - .contentType(ContentType.JSON) - .when() - .post("/user/login") - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); - - RestAssured - .given() - .header("Authorization", "Bearer " + response.jsonPath().getString("token")) - .when() - .post("/user/logout") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + MvcResult result = mockMvc.perform( + post("/user/login") + .content(""" + { + "email": "test@koreatech.ac.kr", + "password": "1234" + } + """ + ) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isCreated()) + .andReturn(); + + JsonNode jsonNode = JsonAssertions.convertJsonNode(result); + mockMvc.perform( + post("/user/logout") + .header("Authorization", "Bearer " + jsonNode.get("token").asText()) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()); Assertions.assertThat(tokenRepository.findById(user.getId())).isEmpty(); } @Test - @DisplayName("사용자가 로그인 이후 refreshToken을 재발급한다") - void userRefreshToken() { + void 사용자가_로그인_이후_refreshToken을_재발급한다() throws Exception { User user = userFixture.builder() .password("1234") .nickname("주노") @@ -131,47 +120,47 @@ void userRefreshToken() { .isDeleted(false) .build(); - var loginResponse = RestAssured - .given() - .body(""" - { - "email": "test@koreatech.ac.kr", - "password": "1234" - } - """) - .contentType(ContentType.JSON) - .when() - .post("/user/login") - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); - - var response = RestAssured - .given() - .body(String.format(""" - { - "refresh_token": "%s" - } - """, - loginResponse.jsonPath().getString("refresh_token")) + MvcResult loginResult = mockMvc.perform( + post("/user/login") + .content(""" + { + "email": "test@koreatech.ac.kr", + "password": "1234" + } + """ + ) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isCreated()) + .andReturn(); + + JsonNode loginJsonNode = JsonAssertions.convertJsonNode(loginResult); + + MvcResult refreshResult = mockMvc.perform( + post("/user/refresh") + .content(String.format(""" + { + "refresh_token": "%s" + } + """, loginJsonNode.get("refresh_token").asText()) + ) + .contentType(MediaType.APPLICATION_JSON) ) - .contentType(ContentType.JSON) - .when() - .post("/user/refresh") - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); + .andExpect(status().isCreated()) + .andReturn(); + + JsonNode refreshJsonNode = JsonAssertions.convertJsonNode(refreshResult); UserToken token = tokenRepository.findById(user.getId()).get(); - JsonAssertions.assertThat(response.asPrettyString()) + JsonAssertions.assertThat(refreshResult.getResponse().getContentAsString()) .isEqualTo(String.format(""" { "token": "%s", "refresh_token": "%s" } """, - response.jsonPath().getString("token"), + refreshJsonNode.get("token").asText(), token.getRefreshToken() )); } diff --git a/src/test/java/in/koreatech/koin/acceptance/BusApiTest.java b/src/test/java/in/koreatech/koin/acceptance/BusApiTest.java index 5f7a42642..09d93fc8b 100644 --- a/src/test/java/in/koreatech/koin/acceptance/BusApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/BusApiTest.java @@ -1,7 +1,8 @@ package in.koreatech.koin.acceptance; import static java.time.format.DateTimeFormatter.ofPattern; -import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import java.time.LocalTime; import java.time.ZonedDateTime; @@ -9,12 +10,15 @@ import java.util.List; import org.assertj.core.api.SoftAssertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.TestConfiguration; -import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.transaction.annotation.Transactional; + +import com.fasterxml.jackson.databind.JsonNode; import in.koreatech.koin.AcceptanceTest; import in.koreatech.koin.domain.bus.dto.SingleBusTimeResponse; @@ -34,10 +38,10 @@ import in.koreatech.koin.domain.version.repository.VersionRepository; import in.koreatech.koin.fixture.BusFixture; import in.koreatech.koin.support.JsonAssertions; -import io.restassured.RestAssured; @SuppressWarnings("NonAsciiCharacters") -@TestConfiguration +@Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class BusApiTest extends AcceptanceTest { @Autowired @@ -52,28 +56,25 @@ class BusApiTest extends AcceptanceTest { @Autowired private ExpressBusCacheRepository expressBusCacheRepository; - @BeforeEach + @BeforeAll void setup() { + clear(); busFixture.버스_시간표_등록(); busFixture.시내버스_시간표_등록(); } @Test - @DisplayName("다음 셔틀버스까지 남은 시간을 조회한다.") - void getNextShuttleBusRemainTime() { - var response = RestAssured - .given() - .when() - .param("bus_type", "shuttle") - .param("depart", "koreatech") - .param("arrival", "terminal") - .get("/bus") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + void 다음_셔틀버스까지_남은_시간을_조회한다() throws Exception { + + mockMvc.perform( + get("/bus") + .param("bus_type", "shuttle") + .param("depart", "koreatech") + .param("arrival", "terminal") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "bus_type": "shuttle", "now_bus": { @@ -82,87 +83,11 @@ void getNextShuttleBusRemainTime() { }, "next_bus": null } - """); - } - - @Test - @DisplayName("다음 시내버스까지 남은 시간을 조회한다. - Redis 캐시 히트") - void getNextCityBusRemainTimeRedis() { - final long remainTime = 600L; - final long busNumber = 400; - BusType busType = BusType.CITY; - BusStation depart = BusStation.TERMINAL; - BusStation arrival = BusStation.KOREATECH; - - BusDirection direction = BusStation.getDirection(depart, arrival); - Version version = versionRepository.save( - Version.builder() - .version("test_version") - .type(VersionType.CITY.getValue()) - .build() - ); - - cityBusCacheRepository.save( - CityBusCache.of( - depart.getNodeId(direction).get(0), - List.of(CityBusCacheInfo.of( - CityBusArrival.builder() - .routeno(busNumber) - .arrtime(remainTime) - .build(), - version.getUpdatedAt()) - ) - ) - ); - - var response = RestAssured - .given() - .when() - .param("bus_type", busType.getName()) - .param("depart", depart.name()) - .param("arrival", arrival.name()) - .get("/bus") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" - { - "bus_type": "city", - "now_bus": { - "bus_number": 400, - "remain_time": 600 - }, - "next_bus": null - } - """); - } - - @Test - @DisplayName("셔틀버스의 코스 정보들을 조회한다.") - void getBusCourses() { - var response = RestAssured - .given() - .when() - .get("/bus/courses") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - assertSoftly( - softly -> { - softly.assertThat(response.body().jsonPath().getList("").size()).isEqualTo(1); - softly.assertThat(response.body().jsonPath().getString("[0].bus_type")).isEqualTo("shuttle"); - softly.assertThat(response.body().jsonPath().getString("[0].direction")).isEqualTo("from"); - softly.assertThat(response.body().jsonPath().getString("[0].region")).isEqualTo("천안"); - } - ); + """)); } @Test - @DisplayName("다음 셔틀버스까지 남은 시간을 조회한다.") - void getSearchTimetable() { + void 도착_시간이_18시_10분인_버스를_정확하게_조회한다() throws Exception { versionRepository.save( Version.builder() .version("test_version") @@ -200,21 +125,22 @@ void getSearchTimetable() { ); expressBusCacheRepository.save(expressBusCache); - var response = RestAssured - .given() - .when() - .param("date", requestedAt.toLocalDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))) - .param("time", requestedAt.toLocalTime().format(DateTimeFormatter.ofPattern("HH:mm"))) - .param("depart", depart.name()) - .param("arrival", arrival.name()) - .get("/bus/search") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + MvcResult result = mockMvc.perform( + get("/bus/search") + .param("date", requestedAt.toLocalDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))) + .param("time", requestedAt.toLocalTime().format(DateTimeFormatter.ofPattern("HH:mm"))) + .param("depart", depart.name()) + .param("arrival", arrival.name()) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andReturn(); SoftAssertions.assertSoftly( softly -> { - softly.assertThat(response.body().jsonPath().getList("", SingleBusTimeResponse.class)) + JsonNode jsonNode = JsonAssertions.convertJsonNode(result); + List actualResponseList = JsonAssertions.convertToList(jsonNode, SingleBusTimeResponse.class); + softly.assertThat(actualResponseList) .containsExactly( new SingleBusTimeResponse("express", LocalTime.parse(arrivalTime)), new SingleBusTimeResponse("shuttle", LocalTime.parse(arrivalTime)), @@ -225,67 +151,79 @@ void getSearchTimetable() { } @Test - @DisplayName("시내버스 시간표를 조회한다 - 지원하지 않음") - void getCityBusTimetable() { - Version version = Version.builder() - .version("test_version") - .type(VersionType.CITY.getValue()) - .build(); - versionRepository.save(version); + void 다음_시내버스까지_남은_시간을_조회한다_Redis_캐시_히트() throws Exception { + final long remainTime = 600L; + final long busNumber = 400; + BusType busType = BusType.CITY; + BusStation depart = BusStation.TERMINAL; + BusStation arrival = BusStation.KOREATECH; - Long busNumber = 400L; - String direction = "종합터미널"; + BusDirection direction = BusStation.getDirection(depart, arrival); + Version version = versionRepository.save( + Version.builder() + .version("test_version") + .type(VersionType.CITY.getValue()) + .build() + ); - var response = RestAssured - .given() - .when() - .param("bus_number", busNumber) - .param("direction", direction) - .get("/bus/timetable/city") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + cityBusCacheRepository.save( + CityBusCache.of( + depart.getNodeId(direction).get(0), + List.of(CityBusCacheInfo.of( + CityBusArrival.builder() + .routeno(busNumber) + .arrtime(remainTime) + .build(), + version.getUpdatedAt()) + ) + ) + ); - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/bus") + .param("bus_type", busType.getName()) + .param("depart", depart.name()) + .param("arrival", arrival.name()) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { - "bus_info": { - "arrival_node": "종합터미널", - "depart_node": "병천3리", - "number": 400 + "bus_type": "city", + "now_bus": { + "bus_number": 400, + "remain_time": 600 }, - "bus_timetables": [ - { - "day_of_week": "평일", - "depart_info": ["06:00", "07:00"] - }, - { - "day_of_week": "주말", - "depart_info": ["08:00", "09:00"] - } - ], - "updated_at": "2024-07-19 19:00:00" + "next_bus": null } - """); + """)); } @Test - @DisplayName("셔틀버스 시간표를 조회한다.") - void getShuttleBusTimetable() { - var response = RestAssured - .given() - .when() - .param("bus_type", "shuttle") - .param("direction", "from") - .param("region", "천안") - .get("/bus/timetable") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + void 셔틀버스의_코스_정보들을_조회한다() throws Exception { + mockMvc.perform( + get("/bus/courses") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()").value(1)) + .andExpect(jsonPath("$[0].bus_type").value("shuttle")) + .andExpect(jsonPath("$[0].direction").value("from")) + .andExpect(jsonPath("$[0].region").value("천안")); + } - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" - [ + @Test + void 셔틀버스_시간표를_조회한다() throws Exception { + mockMvc.perform( + get("/bus/timetable") + .param("bus_type", "shuttle") + .param("direction", "from") + .param("region", "천안") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + [ { "route_name": "주중", "arrival_info": [ @@ -308,12 +246,11 @@ void getShuttleBusTimetable() { ] } ] - """); + """)); } @Test - @DisplayName("셔틀버스 시간표를 조회한다(업데이트 시각 포함).") - void getShuttleBusTimetableWithUpdatedAt() { + void 셔틀버스_시간표를_조회한다_업데이트_시각_포함() throws Exception { Version version = Version.builder() .version("test_version") .type(VersionType.SHUTTLE.getValue()) @@ -324,20 +261,16 @@ void getShuttleBusTimetableWithUpdatedAt() { String direction = "from"; String region = "천안"; - var response = RestAssured - .given() - .when() - .param("bus_type", busType.getName()) - .param("direction", direction) - .param("region", region) - .get("/bus/timetable/v2") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" - { + mockMvc.perform( + get("/bus/timetable/v2") + .param("bus_type", busType.getName()) + .param("direction", direction) + .param("region", region) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { "bus_timetables": [ { "route_name": "주중", @@ -363,6 +296,6 @@ void getShuttleBusTimetableWithUpdatedAt() { ], "updated_at": "2024-01-15 12:00:00" } - """); + """)); } } diff --git a/src/test/java/in/koreatech/koin/acceptance/CircuitBreakerTest.java b/src/test/java/in/koreatech/koin/acceptance/CircuitBreakerTest.java index ce4b8ce6a..58ece6777 100644 --- a/src/test/java/in/koreatech/koin/acceptance/CircuitBreakerTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/CircuitBreakerTest.java @@ -4,11 +4,13 @@ import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.doThrow; -import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Import; +import org.springframework.transaction.annotation.Transactional; import in.koreatech.koin.AcceptanceTest; import in.koreatech.koin.config.TestResilience4jConfig; @@ -17,6 +19,8 @@ import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; @Import(TestResilience4jConfig.class) +@Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class CircuitBreakerTest extends AcceptanceTest { @Autowired @@ -24,7 +28,7 @@ class CircuitBreakerTest extends AcceptanceTest { private CircuitBreaker circuitBreaker; - @BeforeEach + @BeforeAll void setUp() { circuitBreaker = circuitBreakerRegistry.circuitBreaker("test"); circuitBreaker.reset(); diff --git a/src/test/java/in/koreatech/koin/acceptance/CommunityApiTest.java b/src/test/java/in/koreatech/koin/acceptance/CommunityApiTest.java deleted file mode 100644 index db3a6ef92..000000000 --- a/src/test/java/in/koreatech/koin/acceptance/CommunityApiTest.java +++ /dev/null @@ -1,570 +0,0 @@ -package in.koreatech.koin.acceptance; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.time.LocalDate; -import java.util.List; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.skyscreamer.jsonassert.JSONAssert; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import in.koreatech.koin.AcceptanceTest; -import in.koreatech.koin.domain.community.article.model.Article; -import in.koreatech.koin.domain.community.article.model.Board; -import in.koreatech.koin.domain.community.article.model.Comment; -import in.koreatech.koin.domain.community.article.repository.ArticleRepository; -import in.koreatech.koin.domain.community.article.repository.CommentRepository; -import in.koreatech.koin.domain.user.model.Student; -import in.koreatech.koin.fixture.ArticleFixture; -import in.koreatech.koin.fixture.BoardFixture; -import in.koreatech.koin.fixture.UserFixture; -import in.koreatech.koin.support.JsonAssertions; -import io.restassured.RestAssured; - -@SuppressWarnings("NonAsciiCharacters") -class CommunityApiTest extends AcceptanceTest { - - @Autowired - private ArticleRepository articleRepository; - - @Autowired - private CommentRepository commentRepository; - - @Autowired - private UserFixture userFixture; - - @Autowired - private ArticleFixture articleFixture; - - @Autowired - private BoardFixture boardFixture; - - Student student; - Board board; - Article article1, article2; - - @BeforeEach - void givenBeforeEach() { - student = userFixture.준호_학생(); - board = boardFixture.자유게시판(); - article1 = articleFixture.자유글_1(board); - article2 = articleFixture.자유글_2(board); - } - - @Test - @DisplayName("특정 게시글을 단일 조회한다.") - void getArticle() { - // given - Comment request = Comment.builder() - .article(article1) - .content("댓글") - .userId(1) - .nickname("BCSD") - .isDeleted(false) - .build(); - commentRepository.save(request); - - // when then - var response = RestAssured - .given() - .when() - .get("/articles/{articleId}", article1.getId()) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" - { - "id": 1, - "board_id": 1, - "title": "자유 글의 제목입니다", - "content": "

내용

", - "author": "작성자1", - "hit": 1, - "attachments": [ - { - "id": 1, - "name": "첨부파일1.png", - "url": "https://example.com", - "created_at": "2024-01-15 12:00:00", - "updated_at": "2024-01-15 12:00:00" - } - ], - "registered_at": "2024-01-15", - "prev_id": null, - "next_id": 2, - "updated_at": "2024-01-15 12:00:00" - } - """); - } - - @Test - @DisplayName("게시글들을 페이지네이션하여 조회한다.") - void getArticlesByPagination() { - // when then - var response = RestAssured - .given() - .when() - .param("boardId", board.getId()) - .param("page", 1) - .param("limit", 10) - .get("/articles") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" - { - "articles": [ - { - "id": 2, - "board_id": 1, - "title": "자유 글2의 제목입니다", - "author": "작성자2", - "hit": 1, - "registered_at": "2024-01-15", - "updated_at": "2024-01-15 12:00:00" - }, - { - "id": 1, - "board_id": 1, - "title": "자유 글의 제목입니다", - "author": "작성자1", - "hit": 1, - "registered_at": "2024-01-15", - "updated_at": "2024-01-15 12:00:00" - } - ], - "total_count": 2, - "current_count": 2, - "total_page": 1, - "current_page": 1 - } - """); - } - - @Test - @DisplayName("게시글들을 페이지네이션하여 조회한다. - 페이지가 0이면 1 페이지 조회") - void getArticlesByPagination_0Page() { - // when then - var response = RestAssured - .given() - .when() - .param("boardId", board.getId()) - .param("page", 0L) - .param("limit", 1) - .get("/articles") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - assertThat(response.jsonPath().getInt("articles[0].id")).isEqualTo(article2.getId()); - } - - @Test - @DisplayName("게시글들을 페이지네이션하여 조회한다. - 페이지가 음수이면 1 페이지 조회") - void getArticlesByPagination_lessThan0Pages() { - // when then - var response = RestAssured - .given() - .when() - .param("boardId", board.getId()) - .param("page", -10L) - .param("limit", 1) - .get("/articles") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - assertThat(response.jsonPath().getInt("articles[0].id")).isEqualTo(article2.getId()); - } - - @Test - @DisplayName("게시글들을 페이지네이션하여 조회한다. - limit가 0 이면 한 번에 1 게시글 조회") - void getArticlesByPagination_1imit() { - // when then - var response = RestAssured - .given() - .when() - .param("boardId", board.getId()) - .param("page", 1) - .param("limit", 0L) - .get("/articles") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - assertThat(response.jsonPath().getList("articles")).hasSize(1); - } - - @Test - @DisplayName("게시글들을 페이지네이션하여 조회한다. - limit가 음수이면 한 번에 1 게시글 조회") - void getArticlesByPagination_lessThan0Limit() { - // when then - var response = RestAssured - .given() - .when() - .param("boardId", board.getId()) - .param("page", 1) - .param("limit", -10L) - .get("/articles") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - assertThat(response.jsonPath().getList("articles")).hasSize(1); - } - - @Test - @DisplayName("게시글들을 페이지네이션하여 조회한다. - limit가 50 이상이면 한 번에 50 게시글 조회") - void getArticlesByPagination_over50Limit() { - // given - for (int i = 3; i < 63; i++) { // unique 중복 처리 - Article article = Article.builder() - .board(board) - .title("제목") - .content("

내용

") - .author("BCSD") - .hit(14) - .koinHit(0) - .isDeleted(false) - .articleNum(i) - .url("https://example.com") - .registeredAt(LocalDate.of(2024, 1, 15)) - .build(); - articleRepository.save(article); - } - - // when then - var response = RestAssured - .given() - .when() - .param("boardId", board.getId()) - .param("page", 1) - .param("limit", 100L) - .get("/articles") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - assertThat(response.jsonPath().getList("articles")).hasSize(50); - } - - @Test - @DisplayName("게시글들을 페이지네이션하여 조회한다. - 페이지, limit가 주어지지 않으면 1 페이지 10 게시글 조회") - void getArticlesByPagination_default() { - // given - for (int i = 3; i < 13; i++) { // unique 중복 처리 - Article article = Article.builder() - .board(board) - .title("제목") - .content("

내용

") - .author("BCSD") - .hit(14) - .koinHit(0) - .isDeleted(false) - .articleNum(i) - .url("https://example.com") - .registeredAt(LocalDate.of(2024, 1, 15)) - .build(); - articleRepository.save(article); - } - - // when then - var response = RestAssured - .given() - .when() - .param("boardId", board.getId()) - .get("/articles") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - assertThat(response.jsonPath().getList("articles")).hasSize(10); - - } - - @Test - @DisplayName("게시글들을 페이지네이션하여 조회한다. - 특정 페이지 조회") - void getArticlesByPagination_pageTest() { - var response = RestAssured - .given() - .when() - .param("boardId", board.getId()) - .param("page", 2) - .param("limit", 1) - .get("/articles") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" - { - "articles": [ - { - "id": 1, - "board_id": 1, - "title": "자유 글의 제목입니다", - "author": "작성자1", - "hit": 1, - "registered_at": "2024-01-15", - "updated_at": "2024-01-15 12:00:00" - } - ], - "total_count": 2, - "current_count": 1, - "total_page": 2, - "current_page": 2 - } - """); - } - - @Test - @DisplayName("게시글들을 페이지네이션하여 조회한다. - 최대 페이지를 초과한 요청이 들어오면 마지막 페이지를 반환한다.") - void getArticlesByPagination_overMaxPageNotFound() { - // when then - var response = RestAssured - .given() - .when() - .param("boardId", board.getId()) - .param("page", 10000L) - .param("limit", 1) - .get("/articles") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" - { - "articles": [ - { - "id": 1, - "board_id": 1, - "title": "자유 글의 제목입니다", - "author": "작성자1", - "hit": 1, - "registered_at": "2024-01-15", - "updated_at": "2024-01-15 12:00:00" - } - ], - "total_count": 2, - "current_count": 1, - "total_page": 2, - "current_page": 2 - } - """); - } - - @Test - @DisplayName("인기많은 게시글 목록을 조회한다.") - void getHotArticles() { - // given - for (int i = 5; i <= 7; i++) { - articleRepository.save(Article.builder() - .board(board) - .title(String.format("Article %d", i)) - .content("

내용

") - .author("BCSD") - .hit(i) - .koinHit(0) - .isDeleted(false) - .articleNum(i) - .url("https://example.com") - .registeredAt(LocalDate.of(2024, 1, 15)) - .build() - ); - } - - // when then - var response = RestAssured - .given() - .when() - .get("/articles/hot") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" - [ - { - "id": 5, - "board_id": 1, - "title": "Article 7", - "author": "BCSD", - "hit": 7, - "registered_at": "2024-01-15", - "updated_at": "2024-01-15 12:00:00" - }, - { - "id": 4, - "board_id": 1, - "title": "Article 6", - "author": "BCSD", - "hit": 6, - "registered_at": "2024-01-15", - "updated_at": "2024-01-15 12:00:00" - }, - { - "id": 3, - "board_id": 1, - "title": "Article 5", - "author": "BCSD", - "hit": 5, - "registered_at": "2024-01-15", - "updated_at": "2024-01-15 12:00:00" - }, - { - "id": 2, - "board_id": 1, - "title": "자유 글2의 제목입니다", - "author": "작성자2", - "hit": 1, - "registered_at": "2024-01-15", - "updated_at": "2024-01-15 12:00:00" - }, - { - "id": 1, - "board_id": 1, - "title": "자유 글의 제목입니다", - "author": "작성자1", - "hit": 1, - "registered_at": "2024-01-15", - "updated_at": "2024-01-15 12:00:00" - } - ] - """); - } - - @Test - @DisplayName("게시글을 검색한다.") - void searchNoticeArticles() { - var response = RestAssured - .given() - .when() - .queryParam("query", "자유") - .queryParam("board", 1) - .get("/articles/search") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" - { - "articles": [ - { - "id": 2, - "board_id": 1, - "title": "자유 글2의 제목입니다", - "author": "작성자2", - "hit": 1, - "registered_at": "2024-01-15", - "updated_at": "2024-01-15 12:00:00" - }, - { - "id": 1, - "board_id": 1, - "title": "자유 글의 제목입니다", - "author": "작성자1", - "hit": 1, - "registered_at": "2024-01-15", - "updated_at": "2024-01-15 12:00:00" - } - ], - "total_count": 2, - "current_count": 2, - "total_page": 1, - "current_page": 1 - } - """); - } - - @Test - void 사용자들이_많이_검색_한_키워드_추천() { - for (int i = 4; i <= 14; i++) { - Article article = Article.builder() - .board(board) - .title("제목%s".formatted(i)) - .content("

내용333

") - .author("작성자3") - .hit(1) - .koinHit(1) - .isDeleted(false) - .articleNum(i) - .url("https://example3.com") - .attachments(List.of()) - .registeredAt(LocalDate.of(2024, 1, 15)) - .isNotice(false) - .build(); - - articleRepository.save(article); - } - - String ipAddress1 = "192.168.1.1"; - String ipAddress2 = "192.168.1.2"; - String ipAddress3 = "192.168.1.3"; - - for (int i = 4; i < 9; i++) { - RestAssured - .given() - .queryParam("query", "검색어" + i) - .queryParam("board", 1) - .queryParam("page", 1) - .queryParam("limit", 10) - .queryParam("ipAddress", ipAddress1) - .when() - .get("/articles/search") - .then() - .statusCode(HttpStatus.OK.value()); - - RestAssured - .given() - .queryParam("query", "검색어" + i) - .queryParam("board", 1) - .queryParam("page", 1) - .queryParam("limit", 10) - .queryParam("ipAddress", ipAddress2) - .when() - .get("/articles/search") - .then() - .statusCode(HttpStatus.OK.value()); - } - - for (int i = 9; i < 14; i++) { - RestAssured - .given() - .queryParam("query", "검색어" + i) - .queryParam("board", 1) - .queryParam("page", 1) - .queryParam("limit", 10) - .queryParam("ipAddress", ipAddress3) - .when() - .get("/articles/search") - .then() - .statusCode(HttpStatus.OK.value()); - } - - var response = RestAssured - .given() - .queryParam("count", 5) - .when() - .get("/articles/hot/keyword") - .then() - .statusCode(HttpStatus.OK.value()) - .extract() - .asString(); - - assertThat(response).contains("검색어4", "검색어5", "검색어6", "검색어7", "검색어8"); - } -} diff --git a/src/test/java/in/koreatech/koin/acceptance/CoopShopTest.java b/src/test/java/in/koreatech/koin/acceptance/CoopShopTest.java index 89b58734d..07eea0c71 100644 --- a/src/test/java/in/koreatech/koin/acceptance/CoopShopTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/CoopShopTest.java @@ -1,19 +1,24 @@ package in.koreatech.koin.acceptance; -import static io.restassured.RestAssured.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.transaction.annotation.Transactional; import in.koreatech.koin.AcceptanceTest; import in.koreatech.koin.domain.coopshop.model.CoopShop; import in.koreatech.koin.domain.coopshop.repository.CoopShopRepository; import in.koreatech.koin.fixture.CoopShopFixture; -import in.koreatech.koin.support.JsonAssertions; @SuppressWarnings("NonAsciiCharacters") +@Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class CoopShopTest extends AcceptanceTest { @Autowired @@ -25,26 +30,22 @@ class CoopShopTest extends AcceptanceTest { private CoopShop 학생식당; private CoopShop 세탁소; - @BeforeEach + @BeforeAll void setUp() { + clear(); 학생식당 = coopShopFixture.학생식당(); 세탁소 = coopShopFixture.세탁소(); } @Test - public void getCoopShops() { - var response = given() - .when() - .get("/coopshop") - .then() - .log().all() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo( - """ - [ + void 생협의_모든_상점을_조회한다() throws Exception { + mockMvc.perform( + get("/coopshop") + .contentType(MediaType.ALL.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + [ { "id": 1, "name": "학생식당", @@ -104,24 +105,18 @@ public void getCoopShops() { "updated_at" : "2024-01-15" } ] - """ - ); + """)); } @Test - public void getCoopShop() { - var response = given() - .when() - .get("/coopshop/1") - .then() - .log().all() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo( - """ - { + void 생협의_상점을_조회한다() throws Exception { + mockMvc.perform( + get("/coopshop/1") + .contentType(MediaType.ALL.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { "id": 1, "name": "학생식당", "semester" : "하계방학", @@ -155,9 +150,7 @@ public void getCoopShop() { "location": "학생회관 1층", "remarks": "공휴일 휴무", "updated_at" : "2024-01-15" - } - """ - ); - + } + """)); } } diff --git a/src/test/java/in/koreatech/koin/acceptance/DeptApiTest.java b/src/test/java/in/koreatech/koin/acceptance/DeptApiTest.java index 55ff3c7e1..57df819f9 100644 --- a/src/test/java/in/koreatech/koin/acceptance/DeptApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/DeptApiTest.java @@ -1,60 +1,48 @@ package in.koreatech.koin.acceptance; -import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.http.HttpStatus; +import org.junit.jupiter.api.TestInstance; +import org.springframework.http.MediaType; +import org.springframework.transaction.annotation.Transactional; import in.koreatech.koin.AcceptanceTest; import in.koreatech.koin.domain.dept.model.Dept; -import in.koreatech.koin.support.JsonAssertions; -import io.restassured.RestAssured; @SuppressWarnings("NonAsciiCharacters") +@Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class DeptApiTest extends AcceptanceTest { @Test - @DisplayName("학과 번호를 통해 학과 이름을 조회한다.") - void findDeptNameByDeptNumber() { - // given + void 학과_번호를_통해_학과_이름을_조회한다() throws Exception { Dept dept = Dept.COMPUTER_SCIENCE; - - // when then - var response = RestAssured - .given() - .when() - .param("dept_num", dept.getNumbers().get(0)) - .get("/dept") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/dept") + .param("dept_num", dept.getNumbers().get(0)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "dept_num": "35", "name": "컴퓨터공학부" } - """ - ); + """)); } @Test - @DisplayName("모든 학과 정보를 조회한다.") - void findAllDepts() { + void 모든_학과_정보를_조회한다() throws Exception { //given final int DEPT_SIZE = Dept.values().length - 1; - //when then - var response = RestAssured - .given() - .when() - .get("/depts") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - assertThat(response.body().jsonPath().getList(".")).hasSize(DEPT_SIZE); + mockMvc.perform( + get("/depts") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()").value(DEPT_SIZE)); } } diff --git a/src/test/java/in/koreatech/koin/acceptance/DiningApiTest.java b/src/test/java/in/koreatech/koin/acceptance/DiningApiTest.java index 862c04101..fe14df1ab 100644 --- a/src/test/java/in/koreatech/koin/acceptance/DiningApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/DiningApiTest.java @@ -1,18 +1,22 @@ package in.koreatech.koin.acceptance; -import static io.restassured.RestAssured.given; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.time.LocalDate; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.transaction.annotation.Transactional; import in.koreatech.koin.AcceptanceTest; import in.koreatech.koin.domain.coop.model.DiningSoldOutCache; @@ -24,11 +28,10 @@ import in.koreatech.koin.fixture.CoopShopFixture; import in.koreatech.koin.fixture.DiningFixture; import in.koreatech.koin.fixture.UserFixture; -import in.koreatech.koin.support.JsonAssertions; -import io.restassured.RestAssured; -import io.restassured.http.ContentType; @SuppressWarnings("NonAsciiCharacters") +@Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class DiningApiTest extends AcceptanceTest { @Autowired @@ -53,8 +56,9 @@ class DiningApiTest extends AcceptanceTest { private String token_현수; private CoopShop 학생식당; - @BeforeEach + @BeforeAll void setUp() { + clear(); coop_준기 = userFixture.준기_영양사().getUser(); token_준기 = userFixture.getToken(coop_준기); owner_현수 = userFixture.현수_사장님().getUser(); @@ -64,17 +68,13 @@ void setUp() { } @Test - @DisplayName("특정 날짜의 모든 식단들을 조회한다.") - void findDinings() { - var response = given() - .when() - .get("/dinings?date=240115") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + void 특정_날짜의_모든_식단들을_조회한다() throws Exception { + mockMvc.perform( + get("/dinings?date=240115") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" [ { "id": 1, @@ -99,32 +99,27 @@ void findDinings() { "is_liked" : false } ] - """); + """)); } @Test - @DisplayName("잘못된 형식의 날짜로 조회한다. - 날짜의 형식이 잘못되었다면 400") - void invalidFormatDate() { - given() - .when() - .get("/dinings?date=20240115") - .then() - .statusCode(HttpStatus.BAD_REQUEST.value()) - .extract(); + void 잘못된_형식의_날짜로_조회한다_날짜의_형식이_잘못되었다면_400() throws Exception { + + mockMvc.perform( + get("/dinings?date=20240115") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isBadRequest()); } @Test - @DisplayName("날짜가 비어있다. - 오늘 날짜를 받아 조회한다.") - void nullDate() { - var response = given() - .when() - .get("/dinings") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + void 날짜_비어있다_오늘_날짜를_받아_조회한다() throws Exception { + mockMvc.perform( + get("/dinings") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" [ { "id": 1, @@ -149,221 +144,194 @@ void nullDate() { "is_liked" : false } ] - """); + """)); } @Test - @DisplayName("영양사 권한으로 품절 요청을 보낸다.") - void requestSoldOut() { - RestAssured.given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + token_준기) - .body(String.format(""" + void 영양사_권한으로_품절_요청을_보낸다() throws Exception { + mockMvc.perform( + patch("/coop/dining/soldout") + .header("Authorization", "Bearer " + token_준기) + .content(String.format(""" { "menu_id": "%s", "sold_out": %s } - """, A코너_점심.getId(), true) + """, A코너_점심.getId(), true)) + .contentType(MediaType.APPLICATION_JSON) ) - .when() - .patch("/coop/dining/soldout") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + .andExpect(status().isOk()); } @Test - @DisplayName("권한이 없는 사용자가 품절 요청을 보낸다") - void requestSoldOutNoAuth() { - given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + token_현수) - .body(String.format(""" - { - "menu_id": "%s", - "sold_out": %s - } - """, A코너_점심.getId(), true)) - .when() - .patch("/coop/dining/soldout") - .then() - .statusCode(HttpStatus.FORBIDDEN.value()) - .extract(); + void 권한이_없는_사용자가_품절_요청을_보낸다() throws Exception { + mockMvc.perform( + patch("/coop/dining/soldout") + .header("Authorization", "Bearer " + token_현수) + .content(String.format(""" + { + "menu_id": "%s", + "sold_out": %s + } + """, A코너_점심.getId(), true)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isForbidden()); } @Test - @DisplayName("영양사님 권한으로 식단 이미지를 업로드한다. - 이미지 URL이 DB에 저장된다.") - void ImageUpload() { + void 영양사님_권한으로_식단_이미지를_업로드한다_이미지_URL이_DB에_저장된다() throws Exception { String imageUrl = "https://stage.koreatech.in/image.jpg"; - - given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + token_준기) - .body(String.format(""" - { - "menu_id": "%s", - "image_url": "%s" - } - """, A코너_점심.getId(), imageUrl) + mockMvc.perform( + patch("/coop/dining/image") + .header("Authorization", "Bearer " + token_준기) + .content(String.format(""" + { + "menu_id": "%s", + "image_url": "%s" + } + """, A코너_점심.getId(), imageUrl)) + .contentType(MediaType.APPLICATION_JSON) ) - .when() - .patch("/coop/dining/image") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - Dining result = diningRepository.getById(A코너_점심.getId()); - assertThat(result.getImageUrl()).isEqualTo(imageUrl); + .andExpect(status().isOk()) + .andReturn(); + Dining dining = diningRepository.getById(A코너_점심.getId()); + assertThat(dining.getImageUrl()).isEqualTo(imageUrl); } @Test - @DisplayName("허용되지 않은 권한으로 식단 이미지를 업로드한다. - 권한 오류.") - void ImageUploadWithNoAuth() { + void 허용되지_않은_권한으로_식단_이미지를_업로드한다_권한_오류() throws Exception { String imageUrl = "https://stage.koreatech.in/image.jpg"; - - given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + token_현수) - .body(String.format(""" - { - "menu_id": "%s", - "image_url": "%s" - } - """, A코너_점심.getId(), imageUrl) + mockMvc.perform( + patch("/coop/dining/image") + .header("Authorization", "Bearer " + token_현수) + .content(String.format(""" + { + "menu_id": "%s", + "image_url": "%s" + } + """, A코너_점심.getId(), imageUrl)) + .contentType(MediaType.APPLICATION_JSON) ) - .when() - .patch("/coop/dining/image") - .then() - .statusCode(HttpStatus.FORBIDDEN.value()) - .extract(); + .andExpect(status().isForbidden()) + .andReturn(); } @Test - @DisplayName("해당 식사시간에 품절 요청을 한다. - 품절 알림이 발송된다.") - void checkSoldOutNotification() { - given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + token_준기) - .body(String.format(""" - { - "menu_id": "%s", - "sold_out": %s - } - """, A코너_점심.getId(), true)) - .when() - .patch("/coop/dining/soldout") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + void 해당_식사시간에_품절_요청을_한다_품절_알림이_발송된다() throws Exception { + mockMvc.perform( + patch("/coop/dining/soldout") + .header("Authorization", "Bearer " + token_준기) + .content(String.format(""" + { + "menu_id": "%s", + "sold_out": "%s" + } + """, A코너_점심.getId(), true)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andReturn(); - verify(coopEventListener).onDiningSoldOutRequest(any()); + forceVerify(() -> verify(coopEventListener).onDiningSoldOutRequest(any())); + clear(); + setUp(); } @Test - @DisplayName("해당 식사시간 외에 품절 요청을 한다. - 품절 알림이 발송되지 않는다.") - void checkSoldOutNotificationAfterHours() { + void 해당_식사시간_외에_품절_요청을_한다_품절_알림이_발송되지_않는다() throws Exception { Dining A코너_저녁 = diningFixture.A코너_저녁(LocalDate.parse("2024-01-15")); - given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + token_준기) - .body(String.format(""" - { - "menu_id": "%s", - "sold_out": %s - } - """, A코너_저녁.getId(), true)) - .when() - .patch("/coop/dining/soldout") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - verify(coopEventListener, never()).onDiningSoldOutRequest(any()); + mockMvc.perform( + patch("/coop/dining/soldout") + .header("Authorization", "Bearer " + token_준기) + .content(String.format(""" + { + "menu_id": "%s", + "sold_out": "%s" + } + """, A코너_저녁.getId(), true)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andReturn(); + forceVerify(() -> verify(coopEventListener, never()).onDiningSoldOutRequest(any())); + clear(); + setUp(); } @Test - @DisplayName("동일한 식단 코너의 두 번째 품절 요청은 알림이 가지 않는다.") - void checkSoldOutNotificationResend() { + void 동일한_식단_코너의_두_번째_품절_요청은_알림이_가지_않는다() throws Exception { diningSoldOutCacheRepository.save(DiningSoldOutCache.from(A코너_점심.getPlace())); - - given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + token_준기) - .body(String.format(""" - { - "menu_id": "%s", - "sold_out": %s - } - """, A코너_점심.getId(), true)) - .when() - .patch("/coop/dining/soldout") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - verify(coopEventListener, never()).onDiningSoldOutRequest(any()); + mockMvc.perform( + patch("/coop/dining/soldout") + .header("Authorization", "Bearer " + token_준기) + .content(String.format(""" + { + "menu_id": "%s", + "sold_out": "%s" + } + """, A코너_점심.getId(), true)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andReturn(); + forceVerify(() -> verify(coopEventListener, never()).onDiningSoldOutRequest(any())); + clear(); + setUp(); } @Test - @DisplayName("특정 식단의 좋아요를 누른다") - void likeDining() { - RestAssured.given() - .header("Authorization", "Bearer " + token_준기) - .param("diningId", 1) - .when() - .patch("/dining/like") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + void 특정_식단의_좋아요를_누른다() throws Exception { + mockMvc.perform( + patch("/dining/like") + .header("Authorization", "Bearer " + token_준기) + .param("diningId", String.valueOf(1)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andReturn(); } @Test - @DisplayName("특정 식단의 좋아요 중복해서 누르면 에러") - void likeDiningDuplicate() { - RestAssured.given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + token_준기) - .param("diningId", 1) - .when() - .patch("/dining/like") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - RestAssured.given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + token_준기) - .param("diningId", 1) - .when() - .patch("/dining/like") - .then() - .statusCode(HttpStatus.CONFLICT.value()) - .extract(); + void 특정_식단의_좋아요_중복해서_누르면_에러() throws Exception { + mockMvc.perform( + patch("/dining/like") + .header("Authorization", "Bearer " + token_준기) + .param("diningId", String.valueOf(1)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andReturn(); + + mockMvc.perform( + patch("/dining/like") + .header("Authorization", "Bearer " + token_준기) + .param("diningId", String.valueOf(1)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isConflict()) + .andReturn(); } @Test - @DisplayName("좋아요 누른 식단은 isLiked가 true로 반환") - void checkIsLikedTrue() { - RestAssured.given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + token_준기) - .param("diningId", 1) - .when() - .patch("/dining/like") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - var response = given() - .header("Authorization", "Bearer " + token_준기) - .when() - .get("/dinings?date=240115") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + void 좋아요_누른_식단은_isLiked가_true로_반환() throws Exception { + mockMvc.perform( + patch("/dining/like") + .header("Authorization", "Bearer " + token_준기) + .param("diningId", String.valueOf(1)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andReturn(); + + mockMvc.perform( + get("/dinings?date=240115") + .header("Authorization", "Bearer " + token_준기) + .param("diningId", String.valueOf(1)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" [ { "id": 1, @@ -388,22 +356,20 @@ void checkIsLikedTrue() { "is_liked" : true } ] - """); + """)) + .andReturn(); } @Test - @DisplayName("좋아요 안누른 식단은 isLiked가 false로 반환") - void checkIsLikedFalse() { - var response = given() - .when() - .header("Authorization", "Bearer " + token_준기) - .get("/dinings?date=240115") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + void 좋아요_안누른_식단은_isLiked가_false로_반환() throws Exception { + mockMvc.perform( + get("/dinings?date=240115") + .header("Authorization", "Bearer " + token_준기) + .param("diningId", String.valueOf(1)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" [ { "id": 1, @@ -428,68 +394,62 @@ void checkIsLikedFalse() { "is_liked" : false } ] - """); + """)) + .andReturn(); } @Test - @DisplayName("이미지 업로드를 한다. - 품절 알림이 발송된다.") - void checkImageUploadNotification() { + void 이미지_업로드를_한다_품절_알림이_발송된다() throws Exception { String imageUrl = "https://stage.koreatech.in/image.jpg"; - given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + token_준기) - .body(String.format(""" - { - "menu_id": "%s", - "image_url": "%s" - } - """, A코너_점심.getId(), imageUrl)) - .when() - .patch("/coop/dining/image") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - verify(coopEventListener).onDiningImageUploadRequest(any()); + mockMvc.perform( + patch("/coop/dining/image") + .header("Authorization", "Bearer " + token_준기) + .content(String.format(""" + { + "menu_id": "%s", + "image_url": "%s" + } + """, A코너_점심.getId(), imageUrl)) + .param("diningId", String.valueOf(1)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()); + forceVerify(() -> verify(coopEventListener).onDiningImageUploadRequest(any())); + clear(); + setUp(); } @Test - @DisplayName("해당 식사시간 외에 이미지 업로드를 한다. - 품절 알림이 발송되지 않는다.") - void checkImageUploadNotificationAfterHours() { + void 해당_식사시간_외에_이미지_업로드를_한다_품절_알림이_발송되지_않는다() throws Exception { Dining A코너_저녁 = diningFixture.A코너_저녁(LocalDate.parse("2024-01-15")); String imageUrl = "https://stage.koreatech.in/image.jpg"; - - given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + token_준기) - .body(String.format(""" - { - "menu_id": "%s", - "image_url": "%s" - } - """, A코너_저녁.getId(), imageUrl)) - .when() - .patch("/coop/dining/image") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - verify(coopEventListener, never()).onDiningImageUploadRequest(any()); + mockMvc.perform( + patch("/coop/dining/image") + .header("Authorization", "Bearer " + token_준기) + .content(String.format(""" + { + "menu_id": "%s", + "image_url": "%s" + } + """, A코너_저녁.getId(), imageUrl)) + .param("diningId", String.valueOf(1)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()); + forceVerify(() -> verify(coopEventListener, never()).onDiningImageUploadRequest(any())); + clear(); + setUp(); } @Test - @DisplayName("특정 메뉴, 특정 코너의 식단을 검색한다") - void searchDinings() { - var response = given() - .header("Authorization", "Bearer " + token_준기) - .when() - .get("/dinings/search?keyword=육개장&page=1&limit=10&filter=A코너") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + void 특정_메뉴_특정_코너의_식단을_검색한다() throws Exception { + mockMvc.perform( + get("/dinings/search?keyword=육개장&page=1&limit=10&filter=A코너") + .header("Authorization", "Bearer " + token_준기) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "total_count": 1, "current_count": 1, @@ -515,46 +475,44 @@ void searchDinings() { "likes": 0 } ] - } - """); + } + """)) + .andReturn(); } @Test - @DisplayName("특정 메뉴, 특정 코너의 식단을 검색한다 - 해당사항 없을 경우") - void searchDiningsNothing() { - var response = given() - .header("Authorization", "Bearer " + token_준기) - .when() - .get("/dinings/search?keyword=육개장&page=1&limit=10&filter=B코너") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + void 특정_메뉴_특정_코너의_식단을_검색한다_해당사항_없을_경우() throws Exception { + mockMvc.perform( + get("/dinings/search?keyword=육개장&page=1&limit=10&filter=B코너") + .queryParam("keyword", "육개장") + .queryParam("page", "1") + .queryParam("limit", "10") + .queryParam("filter", "B코너") + .header("Authorization", "Bearer " + token_준기) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "total_count": 0, "current_count": 0, "total_page": 0, "current_page": 1, "dinings": [] - } - """); + } + """)) + .andReturn(); } @Test - @DisplayName("특정 메뉴의 식단을 검색한다 - 필터 없을 경우") - void searchDiningsNoFilter() { - var response = given() - .header("Authorization", "Bearer " + token_준기) - .when() - .get("/dinings/search?keyword=육개장&page=1&limit=10&filter=") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + void 특정_메뉴의_식단을_검색한다_필터_없을_경우() throws Exception { + mockMvc.perform( + get("/dinings/search?keyword=육개장&page=1&limit=10&filter=") + .header("Authorization", "Bearer " + token_준기) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "total_count": 1, "current_count": 1, @@ -581,6 +539,7 @@ void searchDiningsNoFilter() { } ] } - """); + """)) + .andReturn(); } } diff --git a/src/test/java/in/koreatech/koin/acceptance/KeywordApiTest.java b/src/test/java/in/koreatech/koin/acceptance/KeywordApiTest.java index 4560dd104..fee434949 100644 --- a/src/test/java/in/koreatech/koin/acceptance/KeywordApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/KeywordApiTest.java @@ -4,13 +4,19 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import java.util.ArrayList; import java.util.List; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.transaction.annotation.Transactional; import in.koreatech.koin.AcceptanceTest; import in.koreatech.koin.domain.community.article.model.Article; @@ -25,11 +31,10 @@ import in.koreatech.koin.fixture.BoardFixture; import in.koreatech.koin.fixture.KeywordFixture; import in.koreatech.koin.fixture.UserFixture; -import in.koreatech.koin.support.JsonAssertions; -import io.restassured.RestAssured; -import io.restassured.http.ContentType; @SuppressWarnings("NonAsciiCharacters") +@Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class KeywordApiTest extends AcceptanceTest { @Autowired @@ -53,115 +58,94 @@ public class KeywordApiTest extends AcceptanceTest { @Autowired private ArticleFixture articleFixture; + private Student 준호_학생; + private String token; + + @BeforeAll + void setup() { + clear(); + 준호_학생 = userFixture.준호_학생(); + token = userFixture.getToken(준호_학생.getUser()); + } + @Test - void 알림_키워드_추가() { - Student student = userFixture.준호_학생(); - String token = userFixture.getToken(student.getUser()); - - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType(ContentType.JSON) - .body(""" - { - "keyword": "장학금" - } - """) - .when() - .post("/articles/keyword") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + void 알림_키워드_추가() throws Exception { + mockMvc.perform( + post("/articles/keyword") + .header("Authorization", "Bearer " + token) + .content(""" + { + "keyword": "장학금" + } + """) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "id": 1, "keyword": "장학금" } - """); + """)); } @Test - void 알림_키워드_10개_넘게_추가시_400에러() { - Student student = userFixture.준호_학생(); - String token = userFixture.getToken(student.getUser()); - + void 알림_키워드_10개_넘게_추가시_400에러() throws Exception { for (int i = 0; i < 10; i++) { - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType(ContentType.JSON) - .body(String.format(""" + mockMvc.perform( + get("/articles/keyword") + .header("Authorization", "Bearer " + token) + .content(String.format(""" { - "keyword": "keyword%d" - } - """, i)) - - .when() - .post("/articles/keyword") - .then() - .statusCode(HttpStatus.OK.value()); + "keyword": "keyword%d" + } + """, i)) + .contentType(MediaType.APPLICATION_JSON) + ); } - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType(ContentType.JSON) - .body(""" - { - "keyword": "장학금" - } - """) - .when() - .post("/articles/keyword") - .then() - .statusCode(HttpStatus.BAD_REQUEST.value()); + mockMvc.perform( + get("/articles/keyword") + .header("Authorization", "Bearer " + token) + .content(""" + { + "keyword": "장학금" + } + """) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isBadRequest()); } @Test - void 알림_키워드_삭제() { - Student student = userFixture.준호_학생(); - String token = userFixture.getToken(student.getUser()); - ArticleKeywordUserMap articleKeywordUserMap = keywordFixture.키워드1("수강 신청", student.getUser()); - - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .pathParam("id", articleKeywordUserMap.getId()) - .contentType(ContentType.JSON) - .when() - .delete("/articles/keyword/{id}") - .then() - .statusCode(HttpStatus.NO_CONTENT.value()) - .extract() - .asString(); + void 알림_키워드_삭제() throws Exception { + ArticleKeywordUserMap articleKeywordUserMap = keywordFixture.키워드1("수강 신청", 준호_학생.getUser()); + + mockMvc.perform( + delete("/articles/keyword/{id}", String.valueOf(articleKeywordUserMap.getId())) + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isNoContent()); assertThat(articleKeywordUserMapRepository.findById(articleKeywordUserMap.getId()).isEmpty()); assertThat(articleKeywordRepository.findById(articleKeywordUserMap.getArticleKeyword().getId()).isEmpty()); } @Test - void 자신의_알림_키워드_조회() { - Student student = userFixture.준호_학생(); - String token = userFixture.getToken(student.getUser()); - ArticleKeywordUserMap articleKeywordUserMap1 = keywordFixture.키워드1("수강신청", student.getUser()); - ArticleKeywordUserMap articleKeywordUserMap2 = keywordFixture.키워드1("장학금", student.getUser()); - ArticleKeywordUserMap articleKeywordUserMap3 = keywordFixture.키워드1("생활관", student.getUser()); - - var response = RestAssured - .given() - .when() - .header("Authorization", "Bearer " + token) - .contentType(ContentType.JSON) - .get("/articles/keyword/me") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" - { + void 자신의_알림_키워드_조회() throws Exception { + ArticleKeywordUserMap articleKeywordUserMap1 = keywordFixture.키워드1("수강신청", 준호_학생.getUser()); + ArticleKeywordUserMap articleKeywordUserMap2 = keywordFixture.키워드1("장학금", 준호_학생.getUser()); + ArticleKeywordUserMap articleKeywordUserMap3 = keywordFixture.키워드1("생활관", 준호_학생.getUser()); + + mockMvc.perform( + get("/articles/keyword/me") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { "count": 3, "keywords": [ { @@ -177,37 +161,28 @@ public class KeywordApiTest extends AcceptanceTest { "keyword": "생활관" } ] - }"""); + } + """)); } @Test - void 사용자_아무_것도_추가_안_했을_때_자신의_알림_키워드_조회_빈_리스트_반환() { - Student student = userFixture.준호_학생(); - String token = userFixture.getToken(student.getUser()); - - var response = RestAssured - .given() - .when() - .header("Authorization", "Bearer " + token) - .contentType(ContentType.JSON) - .get("/articles/keyword/me") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" - { + void 사용자_아무_것도_추가_안_했을_때_자신의_알림_키워드_조회_빈_리스트_반환() throws Exception { + mockMvc.perform( + get("/articles/keyword/me") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { "count": 0, "keywords": [] - }"""); + } + """)); } @Test - void 가장_인기_있는_키워드_추천() { - Student student = userFixture.준호_학생(); - String token1 = userFixture.getToken(student.getUser()); - + void 가장_인기_있는_키워드_추천() throws Exception { // Redis에 인기 키워드 15개 저장 List hotKeywords = new ArrayList<>(); for (int i = 1; i <= 15; i++) { @@ -219,33 +194,25 @@ public class KeywordApiTest extends AcceptanceTest { hotKeywords.forEach(keyword -> articleKeywordSuggestRepository.save(keyword)); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token1) - .contentType(ContentType.JSON) - .when() - .get("/articles/keyword/suggestions") - .then() - .statusCode(HttpStatus.OK.value()) - .extract() - .asPrettyString(); - - JsonAssertions.assertThat(response).isEqualTo(""" - { - "keywords": [ - "수강신청1", "수강신청2", "수강신청3", "수강신청4", "수강신청5", - "수강신청6", "수강신청7", "수강신청8", "수강신청9", "수강신청10", - "수강신청11", "수강신청12", "수강신청13", "수강신청14", "수강신청15" - ] - } - """); + mockMvc.perform( + get("/articles/keyword/suggestions") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { + "keywords": [ + "수강신청1", "수강신청2", "수강신청3", "수강신청4", "수강신청5", + "수강신청6", "수강신청7", "수강신청8", "수강신청9", "수강신청10", + "수강신청11", "수강신청12", "수강신청13", "수강신청14", "수강신청15" + ] + } + """)); } @Test - void 새로운_공지사항이_올라오고_해당_키워드를_갖고_있는_사용자가_있을_경우_알림이_발송된다() { - Student student1 = userFixture.준호_학생(); - Student student2 = userFixture.성빈_학생(); - + void 새로운_공지사항이_올라오고_해당_키워드를_갖고_있는_사용자가_있을_경우_알림이_발송된다() throws Exception { Board board = boardFixture.자유게시판(); List articleIds = new ArrayList<>(); @@ -255,28 +222,27 @@ public class KeywordApiTest extends AcceptanceTest { articleIds.add(article.getId()); } - keywordFixture.키워드1("수강신청1", student1.getUser()); - - RestAssured - .given() - .contentType(ContentType.JSON) - .body(""" - { - "update_notification": %s - } - """.formatted(articleIds.toString())) - .when() - .post("/articles/keyword/notification") - .then() - .statusCode(HttpStatus.OK.value()); - - verify(articleKeywordEventListener).onKeywordRequest(any()); + keywordFixture.키워드1("수강신청1", 준호_학생.getUser()); + + mockMvc.perform( + post("/articles/keyword/notification") + .header("Authorization", "Bearer " + token) + .content(""" + { + "update_notification": %s + } + """.formatted(articleIds.toString())) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()); + forceVerify(() -> verify(articleKeywordEventListener).onKeywordRequest(any())); + clear(); + setup(); } @Test - void 새로운_공지사항이_올라오고_해당_키워드를_갖고_있는_사용자가_없으면_알림이_발송되지_않는다() { - Student student1 = userFixture.준호_학생(); - Student student2 = userFixture.성빈_학생(); + @Transactional + void 새로운_공지사항이_올라오고_해당_키워드를_갖고_있는_사용자가_없으면_알림이_발송되지_않는다() throws Exception { Board board = boardFixture.자유게시판(); @@ -287,21 +253,21 @@ public class KeywordApiTest extends AcceptanceTest { articleIds.add(article.getId()); } - keywordFixture.키워드1("수강신청6", student1.getUser()); - - RestAssured - .given() - .contentType(ContentType.JSON) - .body(""" - { - "update_notification": %s - } - """.formatted(articleIds.toString())) - .when() - .post("/articles/keyword/notification") - .then() - .statusCode(HttpStatus.OK.value()); - - verify(articleKeywordEventListener, never()).onKeywordRequest(any()); + keywordFixture.키워드1("수강신청6", 준호_학생.getUser()); + + mockMvc.perform( + post("/articles/keyword/notification") + .content(""" + { + "update_notification": %s + } + """.formatted(articleIds.toString())) + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()); + forceVerify(() -> verify(articleKeywordEventListener, never()).onKeywordRequest(any())); + clear(); + setup(); } } diff --git a/src/test/java/in/koreatech/koin/acceptance/LandApiTest.java b/src/test/java/in/koreatech/koin/acceptance/LandApiTest.java index 218a64aa3..a3af67b11 100644 --- a/src/test/java/in/koreatech/koin/acceptance/LandApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/LandApiTest.java @@ -1,38 +1,38 @@ package in.koreatech.koin.acceptance; -import org.junit.jupiter.api.DisplayName; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.transaction.annotation.Transactional; import in.koreatech.koin.AcceptanceTest; import in.koreatech.koin.domain.land.model.Land; import in.koreatech.koin.fixture.LandFixture; -import in.koreatech.koin.support.JsonAssertions; -import io.restassured.RestAssured; @SuppressWarnings("NonAsciiCharacters") +@Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class LandApiTest extends AcceptanceTest { @Autowired private LandFixture landFixture; @Test - @DisplayName("복덕방 리스트를 조회한다.") - void getLands() { + void 복덕방_리스트를_조회한다() throws Exception { landFixture.신안빌(); landFixture.에듀윌(); - var response = RestAssured - .given() - .when() - .get("/lands") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/lands") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "lands": [ { @@ -57,24 +57,19 @@ void getLands() { } ] } - """); + """)); } @Test - @DisplayName("복덕방을 단일 조회한다.") - void getLand() { + void 복덕방을_단일_조회한다() throws Exception { Land land = landFixture.에듀윌(); - var response = RestAssured - .given() - .when() - .get("/lands/{id}", land.getId()) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/lands/{id}", land.getId()) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "opt_electronic_door_locks": false, "opt_tv": false, @@ -116,6 +111,6 @@ void getLand() { "permalink": "%EC%97%90", "room_type": "원룸" } - """); + """)); } } diff --git a/src/test/java/in/koreatech/koin/acceptance/MemberApiTest.java b/src/test/java/in/koreatech/koin/acceptance/MemberApiTest.java index 9e621a021..0917824ec 100644 --- a/src/test/java/in/koreatech/koin/acceptance/MemberApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/MemberApiTest.java @@ -1,10 +1,16 @@ package in.koreatech.koin.acceptance; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.transaction.annotation.Transactional; import in.koreatech.koin.AcceptanceTest; import in.koreatech.koin.domain.member.model.Member; @@ -12,9 +18,10 @@ import in.koreatech.koin.domain.member.repository.TrackRepository; import in.koreatech.koin.fixture.MemberFixture; import in.koreatech.koin.support.JsonAssertions; -import io.restassured.RestAssured; @SuppressWarnings("NonAsciiCharacters") +@Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class MemberApiTest extends AcceptanceTest { @Autowired @@ -26,8 +33,9 @@ class MemberApiTest extends AcceptanceTest { Track backend; Track frontend; - @BeforeEach + @BeforeAll void setUp() { + clear(); backend = trackRepository.save( Track.builder() .name("BackEnd") @@ -41,22 +49,20 @@ void setUp() { } @Test - @DisplayName("BCSDLab 회원의 정보를 조회한다") - void getMember() { + void BCSDLab_회원의_정보를_조회한다() throws Exception { Member member = memberFixture.최준호(backend); - var response = RestAssured - .given() - .when() - .get("/members/{id}", member.getId()) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + MvcResult result = mockMvc.perform( + get("/members/{id}", member.getId()) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andReturn(); - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + JsonAssertions.assertThat(result.getResponse().getContentAsString()) + .isEqualTo(""" { - "id": %d, + "id": 1, "name": "최준호", "student_number": "2019136135", "track": "BackEnd", @@ -64,31 +70,23 @@ void getMember() { "email": "testjuno@gmail.com", "image_url": "https://imagetest.com/juno.jpg", "is_deleted": false, - "created_at": "%s", - "updated_at": "%s" - }""", - member.getId(), - response.jsonPath().getString("created_at"), - response.jsonPath().getString("updated_at") - )); + "created_at": "2024-01-15 12:00:00", + "updated_at": "2024-01-15 12:00:00" + }""" + ); } @Test - @DisplayName("BCSDLab 회원들의 정보를 조회한다") - void getMembers() { + void BCSDLab_회원들의_정보를_조회한다() throws Exception { memberFixture.최준호(backend); memberFixture.박한수(frontend); - var response = RestAssured - .given() - .when() - .get("/members") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/members") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" [ { "id": 1, @@ -115,7 +113,6 @@ void getMembers() { "updated_at": "2024-01-15 12:00:00" } ] - """ - ); + """)); } } diff --git a/src/test/java/in/koreatech/koin/acceptance/NotificationApiTest.java b/src/test/java/in/koreatech/koin/acceptance/NotificationApiTest.java index efcede631..1dbff099b 100644 --- a/src/test/java/in/koreatech/koin/acceptance/NotificationApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/NotificationApiTest.java @@ -4,12 +4,16 @@ import static in.koreatech.koin.global.domain.notification.model.NotificationSubscribeType.DINING_SOLD_OUT; import static in.koreatech.koin.global.domain.notification.model.NotificationSubscribeType.SHOP_EVENT; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.transaction.annotation.Transactional; import in.koreatech.koin.AcceptanceTest; import in.koreatech.koin.domain.user.model.User; @@ -19,11 +23,10 @@ import in.koreatech.koin.global.domain.notification.model.NotificationSubscribe; import in.koreatech.koin.global.domain.notification.model.NotificationSubscribeType; import in.koreatech.koin.global.domain.notification.repository.NotificationSubscribeRepository; -import in.koreatech.koin.support.JsonAssertions; -import io.restassured.RestAssured; -import io.restassured.http.ContentType; @SuppressWarnings("NonAsciiCharacters") +@Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class NotificationApiTest extends AcceptanceTest { @Autowired @@ -39,16 +42,16 @@ class NotificationApiTest extends AcceptanceTest { String userToken; String deviceToken; - @BeforeEach + @BeforeAll void setUp() { + clear(); user = userFixture.준호_학생().getUser(); userToken = userFixture.getToken(user); deviceToken = "testToken"; } @Test - @DisplayName("알림 구독 내역을 조회한다.") - void getNotificationSubscribe() { + void 알림_구독_내역을_조회한다() throws Exception { //given NotificationSubscribe notificationSubscribe = NotificationSubscribe.builder() .subscribeType(SHOP_EVENT) @@ -57,18 +60,13 @@ void getNotificationSubscribe() { notificationSubscribeRepository.save(notificationSubscribe); - //when then - var response = RestAssured - .given() - .header("Authorization", "Bearer " + userToken) - .when() - .get("/notification") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + MvcResult result = mockMvc.perform( + get("/notification") + .header("Authorization", "Bearer " + userToken) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "is_permit": false, "subscribes": [ @@ -113,73 +111,66 @@ void getNotificationSubscribe() { } ] } - """); + """)) + .andReturn(); } @Test - @DisplayName("전체 알림을 구독한다. - 디바이스 토큰을 추가한다.") - void createDivceToken() { + void 전체_알림을_구독한다_디바이스_토큰을_추가한다() throws Exception { //when then - RestAssured - .given() - .header("Authorization", "Bearer " + userToken) - .body(String.format(""" - { - "device_token": "%s" - } - """, deviceToken)) - .contentType(ContentType.JSON) - .when() - .post("/notification") - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); - - User result = userRepository.getById(user.getId()); - assertThat(result.getDeviceToken()).isEqualTo(deviceToken); + MvcResult result = mockMvc.perform( + post("/notification") + .content(String.format(""" + { + "device_token": "%s" + } + """, deviceToken)) + .header("Authorization", "Bearer " + userToken) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isCreated()) + .andReturn(); + + User resultUser = userRepository.getById(user.getId()); + assertThat(resultUser.getDeviceToken()).isEqualTo(deviceToken); } @Test - @DisplayName("특정 알림을 구독한다.") - void subscribeNotificationType() { + void 특정_알림을_구독한다() throws Exception { String notificationType = SHOP_EVENT.name(); - RestAssured.given() - .header("Authorization", "Bearer " + userToken) - .body(String.format(""" - { - "device_token": "%s" - } - """, deviceToken)) - .contentType(ContentType.JSON) - .when() - .post("/notification") - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); - - RestAssured - .given() - .header("Authorization", "Bearer " + userToken) - .contentType(ContentType.JSON) - .queryParam("type", notificationType) - .when() - .post("/notification/subscribe") - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); - - var response = RestAssured - .given() - .header("Authorization", "Bearer " + userToken) - .when() - .get("/notification") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + post("/notification") + .content(String.format(""" + { + "device_token": "%s" + } + """, deviceToken)) + .header("Authorization", "Bearer " + userToken) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isCreated()); + + mockMvc.perform( + post("/notification/subscribe") + .content(String.format(""" + { + "device_token": "%s" + } + """, deviceToken)) + .header("Authorization", "Bearer " + userToken) + .queryParam("type", notificationType) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isCreated()); + + mockMvc.perform( + get("/notification") + .header("Authorization", "Bearer " + userToken) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "is_permit": true, "subscribes": [ @@ -224,62 +215,66 @@ void subscribeNotificationType() { } ] } - """); + """)); } @Test - @DisplayName("특정 세부알림을 구독한다.") - void subscribeNotificationDetailType() { + void 특정_세부알림을_구독한다() throws Exception { String notificationType = DINING_SOLD_OUT.name(); String notificationDetailType = LUNCH.name(); - RestAssured.given() - .header("Authorization", "Bearer " + userToken) - .body(String.format(""" - { - "device_token": "%s" - } - """, deviceToken)) - .contentType(ContentType.JSON) - .when() - .post("/notification") - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); - - RestAssured - .given() - .header("Authorization", "Bearer " + userToken) - .contentType(ContentType.JSON) - .queryParam("type", notificationType) - .when() - .post("/notification/subscribe") - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); - - RestAssured - .given() - .header("Authorization", "Bearer " + userToken) - .contentType(ContentType.JSON) - .queryParam("detail_type", notificationDetailType) - .when() - .post("/notification/subscribe/detail") - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); - - var response = RestAssured - .given() - .header("Authorization", "Bearer " + userToken) - .when() - .get("/notification") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + post("/notification") + .content(String.format(""" + { + "device_token": "%s" + } + """, deviceToken)) + .header("Authorization", "Bearer " + userToken) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isCreated()); + + mockMvc.perform( + post("/notification/subscribe") + .queryParam("type", notificationType) + .content(String.format(""" + { + "device_token": "%s" + } + """, deviceToken)) + .header("Authorization", "Bearer " + userToken) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isCreated()); + + mockMvc.perform( + post("/notification/subscribe/detail") + .queryParam("detail_type", notificationDetailType) + .content(String.format(""" + { + "device_token": "%s" + } + """, deviceToken)) + .header("Authorization", "Bearer " + userToken) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isCreated()); + + mockMvc.perform( + get("/notification") + .queryParam("detail_type", notificationDetailType) + .content(String.format(""" + { + "device_token": "%s" + } + """, deviceToken)) + .header("Authorization", "Bearer " + userToken) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { "is_permit": true, "subscribes": [ @@ -324,30 +319,26 @@ void subscribeNotificationDetailType() { } ] } - """); + """)); } @Test - @DisplayName("전체 알림 구독을 취소한다. - 디바이스 토큰을 삭제한다.") - void deleteDeviceToken() { + void 전체_알림_구독을_취소한다_디바이스_토큰을_삭제한다() throws Exception { user.permitNotification(deviceToken); - RestAssured - .given() - .header("Authorization", "Bearer " + userToken) - .when() - .delete("/notification") - .then() - .statusCode(HttpStatus.NO_CONTENT.value()) - .extract(); + mockMvc.perform( + delete("/notification") + .header("Authorization", "Bearer " + userToken) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isNoContent()); User result = userRepository.getById(user.getId()); assertThat(result.getDeviceToken()).isNull(); } @Test - @DisplayName("특정 알림 구독을 취소한다.") - void unsubscribeNotificationType() { + void 특정_알림_구독을_취소한다() throws Exception { var SubscribeShopEvent = NotificationSubscribe.builder() .subscribeType(SHOP_EVENT) .user(user) @@ -363,41 +354,39 @@ void unsubscribeNotificationType() { String notificationType = SHOP_EVENT.name(); - RestAssured.given() - .header("Authorization", "Bearer " + userToken) - .body(String.format(""" - { - "device_token": "%s" - } - """, deviceToken)) - .contentType(ContentType.JSON) - .when() - .post("/notification") - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); - - RestAssured - .given() - .header("Authorization", "Bearer " + userToken) - .queryParam("type", notificationType) - .when() - .delete("/notification/subscribe") - .then() - .statusCode(HttpStatus.NO_CONTENT.value()) - .extract(); - - var response = RestAssured - .given() - .header("Authorization", "Bearer " + userToken) - .when() - .get("/notification") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + post("/notification") + .header("Authorization", "Bearer " + userToken) + .content(String.format(""" + { + "device_token": "%s" + } + """, deviceToken)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isCreated()); + + mockMvc.perform( + delete("/notification/subscribe") + .header("Authorization", "Bearer " + userToken) + .queryParam("type", notificationType) + .content(String.format(""" + { + "device_token": "%s" + } + """, deviceToken)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isNoContent()); + + mockMvc.perform( + get("/notification") + .header("Authorization", "Bearer " + userToken) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { "is_permit": true, "subscribes": [ @@ -442,12 +431,12 @@ void unsubscribeNotificationType() { } ] } - """); + """)); + ; } @Test - @DisplayName("특정 세부 알림 구독을 취소한다.") - void unsubscribeNotificationDetailType() { + void 특정_세부_알림_구독을_취소한다() throws Exception { var SubscribeDiningSoldOut = NotificationSubscribe.builder() .subscribeType(NotificationSubscribeType.DINING_SOLD_OUT) .user(user) @@ -472,52 +461,41 @@ void unsubscribeNotificationDetailType() { String notificationType = DINING_SOLD_OUT.name(); String notificationDetailType = LUNCH.name(); - RestAssured.given() - .header("Authorization", "Bearer " + userToken) - .body(String.format(""" - { - "device_token": "%s" - } - """, deviceToken)) - .contentType(ContentType.JSON) - .when() - .post("/notification") - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); - - RestAssured - .given() - .header("Authorization", "Bearer " + userToken) - .contentType(ContentType.JSON) - .queryParam("type", notificationType) - .when() - .post("/notification/subscribe") - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); - - RestAssured - .given() - .header("Authorization", "Bearer " + userToken) - .queryParam("detail_type", notificationDetailType) - .when() - .delete("/notification/subscribe/detail") - .then() - .statusCode(HttpStatus.NO_CONTENT.value()) - .extract(); - - var response = RestAssured - .given() - .header("Authorization", "Bearer " + userToken) - .when() - .get("/notification") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + post("/notification") + .header("Authorization", "Bearer " + userToken) + .content(String.format(""" + { + "device_token": "%s" + } + """, deviceToken)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isCreated()); + + mockMvc.perform( + post("/notification/subscribe") + .header("Authorization", "Bearer " + userToken) + .queryParam("type", notificationType) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isCreated()); + + mockMvc.perform( + delete("/notification/subscribe/detail") + .header("Authorization", "Bearer " + userToken) + .queryParam("detail_type", notificationDetailType) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isNoContent()); + + mockMvc.perform( + get("/notification") + .header("Authorization", "Bearer " + userToken) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "is_permit": true, "subscribes": [ @@ -562,6 +540,8 @@ void unsubscribeNotificationDetailType() { } ] } - """); + """)); } + + } diff --git a/src/test/java/in/koreatech/koin/acceptance/OwnerApiTest.java b/src/test/java/in/koreatech/koin/acceptance/OwnerApiTest.java index 69cee47a0..785c9a5de 100644 --- a/src/test/java/in/koreatech/koin/acceptance/OwnerApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/OwnerApiTest.java @@ -4,15 +4,20 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.mockito.Mockito.any; import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import org.assertj.core.api.Assertions; import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; import in.koreatech.koin.AcceptanceTest; @@ -26,11 +31,10 @@ import in.koreatech.koin.domain.user.repository.UserRepository; import in.koreatech.koin.fixture.ShopFixture; import in.koreatech.koin.fixture.UserFixture; -import in.koreatech.koin.support.JsonAssertions; -import io.restassured.RestAssured; -import io.restassured.http.ContentType; @SuppressWarnings("NonAsciiCharacters") +@Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class OwnerApiTest extends AcceptanceTest { @Autowired @@ -58,188 +62,162 @@ class OwnerApiTest extends AcceptanceTest { private PasswordEncoder passwordEncoder; @Test - @DisplayName("사장님이 로그인을 진행한다") - void ownerLogin() { + void 사장님이_로그인을_진행한다() throws Exception { Owner owner = userFixture.원경_사장님(); String phoneNumber = owner.getAccount(); String password = "1234"; - var response = RestAssured - .given() - .contentType(ContentType.JSON) - .body(""" - { - "account" : "%s", - "password" : "%s" - } - """.formatted(phoneNumber, password)) - .when() - .post("/owner/login") - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); + mockMvc.perform( + post("/owner/login") + .content(""" + { + "account" : "%s", + "password" : "%s" + } + """.formatted(phoneNumber, password)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isCreated()); } @Test - @DisplayName("관리자가 승인하지 않은 사장님이 로그인을 진행한다") - void unAuthOwnerLogin() { + void 관리자가_승인하지_않은_사장님이_로그인을_진행한다() throws Exception { Owner owner = userFixture.철수_사장님(); String phoneNumber = owner.getAccount(); String password = "1234"; - var response = RestAssured - .given() - .contentType(ContentType.JSON) - .body(""" - { - "account" : "%s", - "password" : "%s" - } - """.formatted(phoneNumber, password)) - .when() - .post("/owner/login") - .then() - .statusCode(HttpStatus.FORBIDDEN.value()) - .extract(); + mockMvc.perform( + post("/owner/login") + .content(""" + { + "account" : "%s", + "password" : "%s" + } + """.formatted(phoneNumber, password)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isForbidden()); } @Test - @DisplayName("로그인된 사장님 정보를 조회한다.") - void getOwner() { + void 로그인된_사장님_정보를_조회한다() throws Exception { // given Owner owner = userFixture.현수_사장님(); Shop shop = shopFixture.마슬랜(owner); String token = userFixture.getToken(owner.getUser()); - // when then - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .get("/owner") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" - { - "email": "hysoo@naver.com", - "name": "테스트용_현수", - "company_number": "123-45-67190", - "account" : "01098765432", - "attachments": [ - { - "id": 1, - "file_url": "https://test.com/현수_사장님_인증사진_1.jpg", - "file_name": "현수_사장님_인증사진_1.jpg" - }, - { - "id": 2, - "file_url": "https://test.com/현수_사장님_인증사진_2.jpg", - "file_name": "현수_사장님_인증사진_2.jpg" - } - ], - "shops": [ - { - "id": %d, - "name": "마슬랜 치킨" - } - ] - } - """, shop.getId() - )); + mockMvc.perform( + get("/owner") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { + "email": "hysoo@naver.com", + "name": "테스트용_현수", + "company_number": "123-45-67190", + "account" : "01098765432", + "attachments": [ + { + "id": 1, + "file_url": "https://test.com/현수_사장님_인증사진_1.jpg", + "file_name": "현수_사장님_인증사진_1.jpg" + }, + { + "id": 2, + "file_url": "https://test.com/현수_사장님_인증사진_2.jpg", + "file_name": "현수_사장님_인증사진_2.jpg" + } + ], + "shops": [ + { + "id": 1, + "name": "마슬랜 치킨" + } + ] + } + """)); } @Test - @DisplayName("사장님이 회원가입 인증번호 전송 요청을 한다 - 전송한 코드로 인증요청이 성공한다") - void requestAndVerifySign() { + void 사장님이_회원가입_인증번호_전송_요청을_한다_전송한_코드로_인증요청이_성공한다() throws Exception { String ownerEmail = "junho5336@gmail.com"; - RestAssured - .given() - .body(String.format(""" - { - "address": "%s" - } - """, ownerEmail) + + mockMvc.perform( + post("/owners/verification/email") + .content(String.format(""" + { + "address": "%s" + } + """, ownerEmail)) + .contentType(MediaType.APPLICATION_JSON) ) - .contentType(ContentType.JSON) - .when() - .post("/owners/verification/email") - .then() - .statusCode(HttpStatus.OK.value()); + .andExpect(status().isOk()); var verifyCode = ownerVerificationStatusRepository.getByVerify(ownerEmail); - RestAssured - .given() - .body(String.format(""" - { - "address": "%s", - "certification_code": "%s" - } - """, ownerEmail, verifyCode.getCertificationCode())) - .contentType(ContentType.JSON) - .when() - .post("/owners/verification/code") - .then() - .statusCode(HttpStatus.OK.value()); - + mockMvc.perform( + post("/owners/verification/code") + .content(String.format(""" + { + "address": "%s", + "certification_code": "%s" + } + """, ownerEmail, verifyCode.getCertificationCode())) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()); var result = ownerVerificationStatusRepository.findById(ownerEmail); Assertions.assertThat(result).isNotPresent(); } @Test - @DisplayName("사장님 회원가입 이메일 인증번호 전송 요청 이벤트 발생 시 슬랙 전송 이벤트가 발생한다.") - void checkOwnerEventListenerByEmail() { - RestAssured - .given() - .body(""" - { - "address": "test@gmail.com" - } - """) - .contentType(ContentType.JSON) - .when() - .post("/owners/verification/email") - .then() - .statusCode(HttpStatus.OK.value()); - - verify(ownerEventListener).onOwnerEmailRequest(any()); + void 사장님_회원가입_이메일_인증번호_전송_요청_이벤트_발생_시_슬랙_전송_이벤트가_발생한다() throws Exception { + mockMvc.perform( + post("/owners/verification/email") + .content(""" + { + "address": "test@gmail.com" + } + """) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()); + forceVerify(() -> verify(ownerEventListener).onOwnerEmailRequest(any())); + clear(); } @Nested @DisplayName("사장님 회원가입") + @Transactional + @TestInstance(TestInstance.Lifecycle.PER_CLASS) class ownerRegister { @Test - @DisplayName("사장님이 이메일로 회원가입 요청을 한다.") - void register() { + void 사장님이_이메일로_회원가입_요청을_한다() throws Exception { // when & then - var response = RestAssured - .given() - .body(""" - { - "attachment_urls": [ - { - "file_url": "https://static.koreatech.in/testimage.png" - } - ], - "company_number": "012-34-56789", - "email": "helloworld@koreatech.ac.kr", - "name": "최준호", - "password": "a0240120305812krlakdsflsa;1235", - "phone_number": "010-0000-0000", - "shop_id": null, - "shop_name": "기분좋은 뷔짱" - } - """) - .contentType(ContentType.JSON) - .when() - .post("/owners/register") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + mockMvc.perform( + post("/owners/register") + .content(""" + { + "attachment_urls": [ + { + "file_url": "https://static.koreatech.in/testimage.png" + } + ], + "company_number": "012-34-56789", + "email": "helloworld@koreatech.ac.kr", + "name": "최준호", + "password": "a0240120305812krlakdsflsa;1235", + "phone_number": "010-0000-0000", + "shop_id": null, + "shop_name": "기분좋은 뷔짱" + } + """) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()); // when transactionTemplate.executeWithoutResult(status -> { @@ -255,7 +233,8 @@ void register() { .isEqualTo("https://static.koreatech.in/testimage.png"); softly.assertThat(owner.getUser().isAuthed()).isFalse(); softly.assertThat(owner.getUser().isDeleted()).isFalse(); - verify(ownerEventListener).onOwnerRegister(any()); + forceVerify(() -> verify(ownerEventListener).onOwnerRegister(any())); + clear(); } ); } @@ -263,32 +242,28 @@ void register() { } @Test - @DisplayName("사장님이 전화번호를 아이디로 회원가입 요청을 한다.") - void registerByPhoneNumber() { + void 사장님이_전화번호를_아이디로_회원가입_요청을_한다() throws Exception { // when & then - var response = RestAssured - .given() - .body(""" - { - "attachment_urls": [ - { - "file_url": "https://static.koreatech.in/testimage.png" - } - ], - "company_number": "012-34-56789", - "name": "최준호", - "password": "a0240120305812krlakdsflsa;1235", - "phone_number": "01012341234", - "shop_id": null, - "shop_name": "기분좋은 뷔짱" - } - """) - .contentType(ContentType.JSON) - .when() - .post("/owners/register/phone") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + mockMvc.perform( + post("/owners/register/phone") + .content(""" + { + "attachment_urls": [ + { + "file_url": "https://static.koreatech.in/testimage.png" + } + ], + "company_number": "012-34-56789", + "name": "최준호", + "password": "a0240120305812krlakdsflsa;1235", + "phone_number": "01012341234", + "shop_id": null, + "shop_name": "기분좋은 뷔짱" + } + """) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()); // when transactionTemplate.executeWithoutResult(status -> { @@ -306,7 +281,8 @@ void registerByPhoneNumber() { .isEqualTo("https://static.koreatech.in/testimage.png"); softly.assertThat(owner.getUser().isAuthed()).isFalse(); softly.assertThat(owner.getUser().isDeleted()).isFalse(); - verify(ownerEventListener).onOwnerRegisterBySms(any()); + forceVerify(() -> verify(ownerEventListener).onOwnerRegisterBySms(any())); + clear(); } ); } @@ -314,120 +290,108 @@ void registerByPhoneNumber() { } @Test - @DisplayName("사장님이 회원가입 요청을 한다 - 첨부파일 이미지 URL이 잘못된 경우 400") - void registerNotAllowedFileUrl() { + void 사장님이_회원가입_요청을_한다_첨부파일_이미지_URL이_잘못된_경우_400() throws Exception { // given - RestAssured - .given() - .body(""" - { - "attachment_urls": [ - { - "file_url": "https://hello.koreatech.in/testimage.png" - } - ], - "company_number": "012-34-56789", - "email": "helloworld@koreatech.ac.kr", - "name": "최준호", - "password": "a0240120305812krlakdsflsa;1235", - "phone_number": "010-0000-0000", - "shop_id": null, - "shop_name": "기분좋은 뷔짱" - } - """) - .contentType(ContentType.JSON) - .when() - .post("/owners/register") - .then() - .statusCode(HttpStatus.BAD_REQUEST.value()); + mockMvc.perform( + post("/owners/register") + .content(""" + { + "attachment_urls": [ + { + "file_url": "https://hello.koreatech.in/testimage.png" + } + ], + "company_number": "012-34-56789", + "email": "helloworld@koreatech.ac.kr", + "name": "최준호", + "password": "a0240120305812krlakdsflsa;1235", + "phone_number": "010-0000-0000", + "shop_id": null, + "shop_name": "기분좋은 뷔짱" + } + """) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isBadRequest()); } @Test - @DisplayName("사장님이 회원가입 요청을 한다 - 잘못된 사업자 등록번호인 경우 400") - void registerNotAllowedCompanyNumber() { + void 사장님이_회원가입_요청을_한다_잘못된_사업자_등록번호인_경우_400() throws Exception { // given - RestAssured - .given() - .body(""" - { - "attachment_urls": [ - { - "file_url": "https://static.koreatech.in/testimage.png" - } - ], - "company_number": "8121-34-56789", - "email": "helloworld@koreatech.ac.kr", - "name": "최준호", - "password": "a0240120305812krlakdsflsa;1235", - "phone_number": "01000000000", - "shop_id": null, - "shop_name": "기분좋은 뷔짱" - } - """) - .contentType(ContentType.JSON) - .when() - .post("/owners/register") - .then() - .statusCode(HttpStatus.BAD_REQUEST.value()); + mockMvc.perform( + post("/owners/register") + .content(""" + { + "attachment_urls": [ + { + "file_url": "https://static.koreatech.in/testimage.png" + } + ], + "company_number": "8121-34-56789", + "email": "helloworld@koreatech.ac.kr", + "name": "최준호", + "password": "a0240120305812krlakdsflsa;1235", + "phone_number": "01000000000", + "shop_id": null, + "shop_name": "기분좋은 뷔짱" + } + """) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isBadRequest()); } @Test - @DisplayName("사장님이 회원가입 요청을 한다 - 이름이 없는경우 400") - void registerWithoutName() { + void 사장님이_회원가입_요청을_한다_이름이_없는경우_400() throws Exception { // given - RestAssured - .given() - .body(""" - { - "attachment_urls": [ - { - "file_url": "https://static.koreatech.in/testimage.png" - } - ], - "company_number": "011-34-56789", - "email": "helloworld@koreatech.ac.kr", - "name": "", - "password": "a0240120305812krlakdsflsa;1235", - "phone_number": "01000000000", - "shop_id": null, - "shop_name": "기분좋은 뷔짱" - } - """) - .contentType(ContentType.JSON) - .when() - .post("/owners/register") - .then() - .statusCode(HttpStatus.BAD_REQUEST.value()); + mockMvc.perform( + post("/owners/register") + .content(""" + { + "attachment_urls": [ + { + "file_url": "https://static.koreatech.in/testimage.png" + } + ], + "company_number": "011-34-56789", + "email": "helloworld@koreatech.ac.kr", + "name": "", + "password": "a0240120305812krlakdsflsa;1235", + "phone_number": "01000000000", + "shop_id": null, + "shop_name": "기분좋은 뷔짱" + } + """) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isBadRequest()); } @Test - @DisplayName("사장님이 회원가입 요청을 한다 - 기존에 존재하는 상점과 함께 회원가입") - void registerWithExistShop() { + void 사장님이_회원가입_요청을_한다_기존에_존재하는_상점과_함께_회원가입() throws Exception { // given Shop shop = shopFixture.마슬랜(null); - RestAssured - .given() - .body(String.format(""" - { - "attachment_urls": [ - { - "file_url": "https://static.koreatech.in/testimage.png" - } - ], - "company_number": "011-34-12312", - "email": "helloworld@koreatech.ac.kr", - "name": "주노", - "password": "a0240120305812krlakdsflsa;1235", - "phone_number": "010-0000-0000", - "shop_id": %d, - "shop_name": "기분좋은 뷔짱" - } - """, shop.getId())) - .contentType(ContentType.JSON) - .when() - .post("/owners/register") - .then() - .statusCode(HttpStatus.OK.value()); + mockMvc.perform( + post("/owners/register") + .content(String.format(""" + { + "attachment_urls": [ + { + "file_url": "https://static.koreatech.in/testimage.png" + } + ], + "company_number": "011-34-12312", + "email": "helloworld@koreatech.ac.kr", + "name": "주노", + "password": "a0240120305812krlakdsflsa;1235", + "phone_number": "010-0000-0000", + "shop_id": %d, + "shop_name": "기분좋은 뷔짱" + } + """, shop.getId())) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()); Owner owner = ownerRepository.findByCompanyRegistrationNumber("011-34-12312").get(); var ownerShop = ownerShopRedisRepository.findById(owner.getId()); @@ -440,32 +404,29 @@ void registerWithExistShop() { } @Test - @DisplayName("사장님이 회원가입 요청을 한다 - 존재하지 않는 상점과 함께 회원가입") - void registerWithNotExistShop() { + void 사장님이_회원가입_요청을_한다_존재하지_않는_상점과_함께_회원가입() throws Exception { // given - RestAssured - .given() - .body(""" - { - "attachment_urls": [ - { - "file_url": "https://static.koreatech.in/testimage.png" - } - ], - "company_number": "011-34-56789", - "email": "helloworld@koreatech.ac.kr", - "name": "주노", - "password": "a0240120305812krlakdsflsa;1235", - "phone_number": "010-0000-0000", - "shop_id": null, - "shop_name": "기분좋은 뷔짱" - } - """) - .contentType(ContentType.JSON) - .when() - .post("/owners/register") - .then() - .statusCode(HttpStatus.OK.value()); + mockMvc.perform( + post("/owners/register") + .content(""" + { + "attachment_urls": [ + { + "file_url": "https://static.koreatech.in/testimage.png" + } + ], + "company_number": "011-34-56789", + "email": "helloworld@koreatech.ac.kr", + "name": "주노", + "password": "a0240120305812krlakdsflsa;1235", + "phone_number": "010-0000-0000", + "shop_id": null, + "shop_name": "기분좋은 뷔짱" + } + """) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()); Owner owner = ownerRepository.findByCompanyRegistrationNumber("011-34-56789").get(); var ownerShop = ownerShopRedisRepository.findById(owner.getId()); assertSoftly( @@ -478,133 +439,111 @@ void registerWithNotExistShop() { } @Test - @DisplayName("사장님이 회원가입 인증번호를 확인한다") - void ownerCodeVerification() { + void 사장님이_회원가입_인증번호를_확인한다() throws Exception { // given OwnerVerificationStatus verification = OwnerVerificationStatus.of("junho5336@gmail.com", "123456"); ownerVerificationStatusRepository.save(verification); - RestAssured - .given() - .body(""" - { - "address": "junho5336@gmail.com", - "certification_code": "123456" - } - """) - .contentType(ContentType.JSON) - .when() - .post("/owners/verification/code") - .then() - .statusCode(HttpStatus.OK.value()); + mockMvc.perform( + post("/owners/verification/code") + .content(""" + { + "address": "junho5336@gmail.com", + "certification_code": "123456" + } + """) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()); var result = ownerVerificationStatusRepository.findById(verification.getKey()); assertThat(result).isNotPresent(); } @Test - @DisplayName("사장님이 회원가입 인증번호를 확인한다 - 존재하지 않는 이메일로 요청을 보낸다") - void ownerCodeVerificationNotExistEmail() { + void 사장님이_회원가입_확인한다_존재하지_않는_이메일로_요청을_보낸다() throws Exception { // given OwnerVerificationStatus verification = OwnerVerificationStatus.of("junho5336@gmail.com", "123456"); ownerVerificationStatusRepository.save(verification); - RestAssured - .given() - .body(""" - { - "address": "someone@gmail.com", - "certification_code": "123456" - } - """) - .contentType(ContentType.JSON) - .when() - .post("/owners/verification/code") - .then() - .statusCode(HttpStatus.NOT_FOUND.value()); + mockMvc.perform( + post("/owners/verification/code") + .content(""" + { + "address": "someone@gmail.com", + "certification_code": "123456" + } + """) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isNotFound()); } @Test - @DisplayName("사장님이 비밀번호 변경을 위한 인증번호 이메일을 전송을 요청한다") - void sendResetPasswordEmail() { + void 사장님이_비밀번호_변경을_위한_인증번호_이메일을_전송을_요청한다() throws Exception { // given Owner owner = userFixture.현수_사장님(); ownerRepository.save(owner); - RestAssured - .given() - .body(String.format(""" - { - "address": "%s" - } - """, owner.getUser().getEmail()) + mockMvc.perform( + post("/owners/password/reset/verification") + .content(String.format(""" + { + "address": "%s" + } + """, owner.getUser().getEmail())) + .contentType(MediaType.APPLICATION_JSON) ) - .contentType(ContentType.JSON) - .when() - .post("/owners/password/reset/verification") - .then() - .statusCode(HttpStatus.OK.value()); - + .andExpect(status().isOk()); assertThat(ownerVerificationStatusRepository.findById(owner.getUser().getEmail())).isPresent(); } @Test - @DisplayName("사장님이 비밀번호 변경을 위한 인증번호 이메일을 전송을 하루 요청 횟수(5번)를 초과하여 요청한다 - 400에러를 반환한다.") - void sendResetPasswordEmailWithDailyLimit() { + void 사장님이_비밀번호_변경을_위한_인증번호_이메일을_전송을_하루_요청_횟수_5번_를_초과하여_요청한다_400에러를_반환한다() throws Exception { // given int DAILY_LIMIT = 5; Owner owner = userFixture.현수_사장님(); ownerRepository.save(owner); for (int i = 0; i < DAILY_LIMIT; ++i) { - RestAssured - .given() - .body(String.format(""" - { - "address": "%s" - } - """, owner.getUser().getEmail()) + mockMvc.perform( + post("/owners/password/reset/verification") + .content(String.format(""" + { + "address": "%s" + } + """, owner.getUser().getEmail())) + .contentType(MediaType.APPLICATION_JSON) ) - .contentType(ContentType.JSON) - .when() - .post("/owners/password/reset/verification") - .then() - .statusCode(HttpStatus.OK.value()); + .andExpect(status().isOk()); } - - RestAssured - .given() - .body(String.format(""" - { - "address": "%s" - } - """, owner.getUser().getEmail()) + mockMvc.perform( + post("/owners/password/reset/verification") + .content(String.format(""" + { + "address": "%s" + } + """, owner.getUser().getEmail())) + .contentType(MediaType.APPLICATION_JSON) ) - .contentType(ContentType.JSON) - .when() - .post("/owners/password/reset/verification") - .then() - .statusCode(HttpStatus.BAD_REQUEST.value()); + .andExpect(status().isBadRequest()); } @Test - @DisplayName("사장님이 인증번호를 확인한다.") - void ownerVerify() { + void 사장님이_인증번호를_확인한다() throws Exception { // given String email = "test@test.com"; String code = "123123"; OwnerVerificationStatus verification = OwnerVerificationStatus.of(email, code); ownerVerificationStatusRepository.save(verification); - RestAssured - .given() - .body(String.format(""" - { - "address": "%s", - "certification_code": "%s" - } - """, email, code) + mockMvc.perform( + post("/owners/password/reset/send") + .content(String.format(""" + { + "address": "%s", + "certification_code": "%s" + } + """, email, code)) + .contentType(MediaType.APPLICATION_JSON) ) - .contentType(ContentType.JSON) - .when() - .post("/owners/password/reset/send") - .then() - .statusCode(HttpStatus.OK.value()); + .andExpect(status().isOk()); + var result = ownerVerificationStatusRepository.findById(email); assertSoftly( softly -> { @@ -615,49 +554,40 @@ void ownerVerify() { } @Test - @DisplayName("사장님이 인증번호를 확인한다. - 중복 시 404를 반환한다.") - void ownerVerifyDuplicated() { + void 사장님이_인증번호를_확인한다_중복_시_404를_반환한다() throws Exception { // given String email = "test@test.com"; String code = "123123"; OwnerVerificationStatus verification = OwnerVerificationStatus.of(email, code); ownerVerificationStatusRepository.save(verification); // when - RestAssured - .given() - .body(String.format(""" - { - "address": "%s", - "certification_code": "%s" - } - """, email, code) + mockMvc.perform( + post("/owners/password/reset/send") + .content(String.format(""" + { + "address": "%s", + "certification_code": "%s" + } + """, email, code)) + .contentType(MediaType.APPLICATION_JSON) ) - .contentType(ContentType.JSON) - .when() - .post("/owners/password/reset/send") - .then() - .statusCode(HttpStatus.OK.value()); + .andExpect(status().isOk()); - // then - RestAssured - .given() - .body(String.format(""" - { - "address": "%s", - "certification_code": "%s" - } - """, email, code) + mockMvc.perform( + post("/owners/password/reset/send") + .content(String.format(""" + { + "address": "%s", + "certification_code": "%s" + } + """, email, code)) + .contentType(MediaType.APPLICATION_JSON) ) - .contentType(ContentType.JSON) - .when() - .post("/owners/password/reset/send") - .then() - .statusCode(HttpStatus.NOT_FOUND.value()); + .andExpect(status().isNotFound()); } @Test - @DisplayName("사장님이 비밀번호를 변경한다.") - void ownerChangePassword() { + void 사장님이_비밀번호를_변경한다() throws Exception { // given User user = userFixture.현수_사장님().getUser(); String code = "123123"; @@ -665,37 +595,29 @@ void ownerChangePassword() { ownerVerificationStatusRepository.save(verification); String password = "asdf1234!"; - RestAssured - .given() - .body(String.format(""" - { - "address": "%s", - "certification_code": "%s" - } - """, user.getEmail(), code) + mockMvc.perform( + post("/owners/password/reset/send") + .content(String.format(""" + { + "address": "%s", + "certification_code": "%s" + } + """, user.getEmail(), code)) + .contentType(MediaType.APPLICATION_JSON) ) - .contentType(ContentType.JSON) - .when() - .post("/owners/password/reset/send") - .then() - .statusCode(HttpStatus.OK.value()); - - // when - RestAssured - .given() - .body(String.format(""" - { - "address": "%s", - "password": "%s" - } - """, user.getEmail(), password) + .andExpect(status().isOk()); + + mockMvc.perform( + put("/owners/password/reset") + .content(String.format(""" + { + "address": "%s", + "password": "%s" + } + """, user.getEmail(), password)) + .contentType(MediaType.APPLICATION_JSON) ) - .contentType(ContentType.JSON) - .when() - .put("/owners/password/reset") - .then() - .statusCode(HttpStatus.OK.value()); - + .andExpect(status().isOk()); // then var result = ownerVerificationStatusRepository.findById(user.getEmail()); User userResult = userRepository.getByEmail(user.getEmail()); @@ -708,135 +630,104 @@ void ownerChangePassword() { } @Test - @DisplayName("사장님이 회원탈퇴를 한다.") - void ownerDelete() { + void 사장님이_회원탈퇴를_한다() throws Exception { // given Owner owner = userFixture.현수_사장님(); String token = userFixture.getToken(owner.getUser()); - // when - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .delete("/user") - .then() - .statusCode(HttpStatus.NO_CONTENT.value()) - .extract(); - + mockMvc.perform( + MockMvcRequestBuilders.delete("/user") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isNoContent()); // then assertThat(userRepository.findById(owner.getId())).isNotPresent(); } @Test - @DisplayName("사업자 등록번호 중복 검증 - 존재하지 않으면 200") - void checkDuplicateCompanyNumber() { + void 사업자_등록번호_중복_검증_존재하지_않으면_200() throws Exception { // when & then - RestAssured - .given() - .queryParam("company_number", "123-45-67190") - .when() - .get("/owners/exists/company-number") - .then() - .statusCode(HttpStatus.OK.value()); + mockMvc.perform( + get("/owners/exists/company-number") + .queryParam("company_number", "123-45-67190") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()); } @Test - @DisplayName("사업자 등록번호 중복 검증 - 이미 존재하면 409") - void checkDuplicateCompanyNumberExists() { + void 사업자_등록번호_중복_검증_이미_존재하면_409() throws Exception { // given Owner owner = userFixture.현수_사장님(); // when & then - var response = RestAssured - .given() - .queryParam("company_number", owner.getCompanyRegistrationNumber()) - .when() - .get("/owners/exists/company-number") - .then() - .statusCode(HttpStatus.CONFLICT.value()) - .extract(); - - assertThat(response.body().jsonPath().getString("message")) - .isEqualTo("이미 존재하는 사업자 등록번호입니다."); + mockMvc.perform( + get("/owners/exists/company-number") + .queryParam("company_number", owner.getCompanyRegistrationNumber()) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isConflict()); } @Test - @DisplayName("사업자 등록번호 중복 검증 - 값이 존재하지 않으면 400") - void checkDuplicateCompanyNumberNotAccept() { + void 사업자_등록번호_중복_검증_값이_존재하지_않으면_400() throws Exception { // when & then - RestAssured - .given() - .when() - .get("/owners/exists/company-number") - .then() - .statusCode(HttpStatus.BAD_REQUEST.value()); + mockMvc.perform( + get("/owners/exists/company-number") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isBadRequest()); } @Test - @DisplayName("사업자 등록번호 중복 검증 - 값이 올바르지 않으면 400") - void checkDuplicateCompanyNumberNotMatchedPattern() { + void 사업자_등록번호_중복_검증_값이_올바르지_않으면_400() throws Exception { // when & then - RestAssured - .given() - .queryParam("company_number", "1234567890") - .when() - .get("/owners/exists/company-number") - .then() - .statusCode(HttpStatus.BAD_REQUEST.value()); + mockMvc.perform( + get("/owners/exists/company-number") + .queryParam("company_number", "1234567890") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isBadRequest()); } @Test - @DisplayName("사장님 아이디(전화번호) 중복 검증 - 존재하지 않으면 200") - void checkExistsPhoneNumber() { - RestAssured - .given() - .param("account", "01012345678") - .when() - .get("/owners/exists/account") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + void 사장님_아이디_전화번호_중복_검증_존재하지_않으면_200() throws Exception { + mockMvc.perform( + get("/owners/exists/account") + .param("account", "01012345678") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()); } @Test - @DisplayName("사장님 아이디(전화번호) 중복 검증 - 이미 존재하면 409") - void checkExistsPhoneNumberConflict() { + void 사장님_아이디_전화번호_중복_검증_이미_존재하면_409() throws Exception { Owner owner = userFixture.현수_사장님(); - var response = RestAssured - .given() - .param("account", owner.getAccount()) - .when() - .get("/owners/exists/account") - .then() - .statusCode(HttpStatus.CONFLICT.value()) - .extract(); - - assertThat(response.body().jsonPath().getString("message")) - .contains("이미 존재하는 휴대폰번호입니다."); + mockMvc.perform( + get("/owners/exists/account") + .param("account", owner.getAccount()) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isConflict()); } @Test - @DisplayName("사장님 아이디(전화번호) 중복 검증 - 파라미터에 전화번호를 포함하지 않으면 400") - void checkExistsPhoneNumberNull() { - RestAssured - .when() - .get("/owners/exists/account") - .then() - .statusCode(HttpStatus.BAD_REQUEST.value()) - .extract(); + void 사장님_아이디_전화번호_중복_검증_파라미터에_전화번호를_포함하지_않으면_400() throws Exception { + mockMvc.perform( + get("/owners/exists/account") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isBadRequest()); } @Test - @DisplayName("사장님 아이디(전화번호) 중복 검증 - 잘못된 전화번호 형식이면 400") - void checkExistsPhoneNumberWrongFormat() { + void 사장님_아이디_전화번호_중복_검증_잘못된_전화번호_형식이면_400() throws Exception { String phoneNumber = "123123123123"; - RestAssured - .given() - .param("phone_number", phoneNumber) - .when() - .get("/owners/exists/account") - .then() - .statusCode(HttpStatus.BAD_REQUEST.value()) - .extract(); + mockMvc.perform( + get("/owners/exists/account") + .param("phone_number", phoneNumber) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isBadRequest()); } } diff --git a/src/test/java/in/koreatech/koin/acceptance/OwnerShopApiTest.java b/src/test/java/in/koreatech/koin/acceptance/OwnerShopApiTest.java index d35978a6f..35019bf6a 100644 --- a/src/test/java/in/koreatech/koin/acceptance/OwnerShopApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/OwnerShopApiTest.java @@ -3,20 +3,23 @@ import static java.time.format.DateTimeFormatter.ofPattern; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.hamcrest.Matchers.hasSize; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import java.time.LocalDate; import java.util.List; import java.util.Optional; import java.util.Set; -import org.assertj.core.api.SoftAssertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.transaction.support.TransactionTemplate; import in.koreatech.koin.AcceptanceTest; @@ -42,11 +45,11 @@ import in.koreatech.koin.fixture.ShopCategoryFixture; import in.koreatech.koin.fixture.ShopFixture; import in.koreatech.koin.fixture.UserFixture; -import in.koreatech.koin.support.JsonAssertions; -import io.restassured.RestAssured; -import io.restassured.http.ContentType; +import jakarta.transaction.Transactional; +@Transactional @SuppressWarnings("NonAsciiCharacters") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class OwnerShopApiTest extends AcceptanceTest { @Autowired @@ -92,8 +95,9 @@ class OwnerShopApiTest extends AcceptanceTest { private MenuCategory menuCategory_메인; private MenuCategory menuCategory_사이드; - @BeforeEach + @BeforeAll void setUp() { + clear(); owner_현수 = userFixture.현수_사장님(); token_현수 = userFixture.getToken(owner_현수.getUser()); owner_준영 = userFixture.준영_사장님(); @@ -106,23 +110,15 @@ void setUp() { } @Test - @DisplayName("사장님의 가게 목록을 조회한다.") - void getOwnerShops() { + void 사장님의_가게_목록을_조회한다() throws Exception { // given - shopFixture.영업중이_아닌_신전_떡볶이(owner_현수); - - // when then - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token_현수) - .when() - .get("/owner/shops") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + shopFixture.배달_안되는_신전_떡볶이(owner_현수); + mockMvc.perform( + get("/owner/shops") + .header("Authorization", "Bearer " + token_현수) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "count": 2, "shops": [ @@ -138,122 +134,107 @@ void getOwnerShops() { } ] } - """); + """) + ); } @Test - @DisplayName("상점을 생성한다.") - void createOwnerShop() { + void 상점을_생성한다() throws Exception { // given - RestAssured - .given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + token_현수) - .body(String.format(""" - { - "address": "대전광역시 유성구 대학로 291", - "category_ids": [ - %d - ], - "delivery": true, - "delivery_price": 4000, - "description": "테스트 상점2입니다.", - "image_urls": [ - "https://test.com/test1.jpg", - "https://test.com/test2.jpg", - "https://test.com/test3.jpg" - ], - - "name": "테스트 상점2", - "open": [ - { - "close_time": "21:00", - "closed": false, - "day_of_week": "MONDAY", - "open_time": "09:00" - }, - { - "close_time": "21:00", - "closed": false, - "day_of_week": "TUESDAY", - "open_time": "09:00" - }, + mockMvc.perform( + post("/owner/shops") + .header("Authorization", "Bearer " + token_현수) + .contentType(MediaType.APPLICATION_JSON) + .content(String.format(""" { - "close_time": "21:00", - "closed": false, - "day_of_week": "WEDNESDAY", - "open_time": "09:00" - }, - { - "close_time": "21:00", - "closed": false, - "day_of_week": "THURSDAY", - "open_time": "09:00" - }, - { - "close_time": "21:00", - "closed": false, - "day_of_week": "FRIDAY", - "open_time": "09:00" - }, - { - "close_time": "21:00", - "closed": false, - "day_of_week": "SATURDAY", - "open_time": "09:00" - }, - { - "close_time": "21:00", - "closed": false, - "day_of_week": "SUNDAY", - "open_time": "09:00" + "address": "대전광역시 유성구 대학로 291", + "category_ids": [ + %d + ], + "delivery": true, + "delivery_price": 4000, + "description": "테스트 상점2입니다.", + "image_urls": [ + "https://test.com/test1.jpg", + "https://test.com/test2.jpg", + "https://test.com/test3.jpg" + ], + "name": "테스트 상점2", + "open": [ + { + "close_time": "21:00", + "closed": false, + "day_of_week": "MONDAY", + "open_time": "09:00" + }, + { + "close_time": "21:00", + "closed": false, + "day_of_week": "TUESDAY", + "open_time": "09:00" + }, + { + "close_time": "21:00", + "closed": false, + "day_of_week": "WEDNESDAY", + "open_time": "09:00" + }, + { + "close_time": "21:00", + "closed": false, + "day_of_week": "THURSDAY", + "open_time": "09:00" + }, + { + "close_time": "21:00", + "closed": false, + "day_of_week": "FRIDAY", + "open_time": "09:00" + }, + { + "close_time": "21:00", + "closed": false, + "day_of_week": "SATURDAY", + "open_time": "09:00" + }, + { + "close_time": "21:00", + "closed": false, + "day_of_week": "SUNDAY", + "open_time": "09:00" + } + ], + "pay_bank": true, + "pay_card": true, + "phone": "010-1234-5678" } - ], - "pay_bank": true, - "pay_card": true, - "phone": "010-1234-5678" - } - """, shopCategory_치킨.getId()) + """, shopCategory_치킨.getId()) + ) ) - .when() - .post("/owner/shops") - .then() - .log().all() - .statusCode(HttpStatus.CREATED.value()) - .extract(); - - transactionTemplate.executeWithoutResult(status -> { - List shops = shopRepository.findAllByOwnerId(owner_현수.getId()); - Shop result = shops.get(1); - assertSoftly( - softly -> { - softly.assertThat(result.getAddress()).isEqualTo("대전광역시 유성구 대학로 291"); - softly.assertThat(result.getDeliveryPrice()).isEqualTo(4000); - softly.assertThat(result.getDescription()).isEqualTo("테스트 상점2입니다."); - softly.assertThat(result.getName()).isEqualTo("테스트 상점2"); - softly.assertThat(result.getShopImages()).hasSize(3); - softly.assertThat(result.getShopOpens()).hasSize(7); - softly.assertThat(result.getShopCategories()).hasSize(1); - } - ); - }); + .andExpect(status().isCreated()); + List shops = shopRepository.findAllByOwnerId(owner_현수.getId()); + Shop result = shops.get(1); + assertSoftly( + softly -> { + softly.assertThat(result.getAddress()).isEqualTo("대전광역시 유성구 대학로 291"); + softly.assertThat(result.getDeliveryPrice()).isEqualTo(4000); + softly.assertThat(result.getDescription()).isEqualTo("테스트 상점2입니다."); + softly.assertThat(result.getName()).isEqualTo("테스트 상점2"); + softly.assertThat(result.getShopImages()).hasSize(3); + softly.assertThat(result.getShopOpens()).hasSize(7); + softly.assertThat(result.getShopCategories()).hasSize(1); + System.out.println("dsa"); + } + ); } @Test - @DisplayName("상점 사장님이 특정 상점 조회") - void getShop() { - // given - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token_현수) - .when() - .get("/owner/shops/{shopId}", shop_마슬랜.getId()) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + void 상점_사장님이_특정_상점_조회() throws Exception { + mockMvc.perform( + get("/owner/shops/{shopId}", shop_마슬랜.getId()) + .header("Authorization", "Bearer " + token_현수) + ).andExpect(status().isOk()) + .andExpect(content().json(""" { "address": "천안시 동남구 병천면 1600", "delivery": true, @@ -293,33 +274,25 @@ void getShop() { "pay_card": true, "phone": "010-7574-1212", "shop_categories": [ - ], "updated_at": "2024-01-15", "is_event": false, "bank": "국민", "account_number": "01022595923" } - """); + """)); } @Test - @DisplayName("특정 상점의 모든 메뉴를 조회한다.") - void findOwnerShopMenu() { - // given + void 특정_상점의_모든_메뉴를_조회한다() throws Exception { menuFixture.짜장면_옵션메뉴(shop_마슬랜, menuCategory_메인); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token_현수) - .param("shopId", shop_마슬랜.getId()) - .when() - .get("/owner/shops/menus") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/owner/shops/menus") + .header("Authorization", "Bearer " + token_현수) + .param("shopId", shop_마슬랜.getId().toString()) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "count": 1, "menu_categories": [ @@ -354,26 +327,20 @@ void findOwnerShopMenu() { ], "updated_at": "2024-01-15" } - """); + """)); } @Test - @DisplayName("사장님이 자신의 상점 메뉴 카테고리들을 조회한다.") - void findOwnerMenuCategories() { + void 사장님이_자신의_상점_메뉴_카테고리들을_조회한다() throws Exception { // given menuFixture.짜장면_단일메뉴(shop_마슬랜, menuCategory_메인); - - var response = RestAssured - .given() - .param("shopId", shop_마슬랜.getId()) - .header("Authorization", "Bearer " + token_현수) - .when() - .get("/owner/shops/menus/categories") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/owner/shops/menus/categories") + .header("Authorization", "Bearer " + token_현수) + .param("shopId", shop_마슬랜.getId().toString()) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "count": 2, "menu_categories": [ @@ -387,178 +354,137 @@ void findOwnerMenuCategories() { } ] } - """); + """)); } @Test - @DisplayName("사장님이 자신의 상점의 특정 메뉴를 조회한다.") - void findMenuShopOwner() { + void 사장님이_자신의_상점의_특정_메뉴를_조회한다() throws Exception { // given Menu menu = menuFixture.짜장면_옵션메뉴(shop_마슬랜, menuCategory_메인); - - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token_현수) - .when() - .get("/owner/shops/menus/{menuId}", menu.getId()) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - SoftAssertions.assertSoftly( - softly -> { - softly.assertThat(response.body().jsonPath().getInt("id")).isEqualTo(menu.getId()); - - softly.assertThat(response.body().jsonPath().getInt("shop_id")).isEqualTo(menu.getShopId()); - softly.assertThat(response.body().jsonPath().getString("name")).isEqualTo(menu.getName()); - softly.assertThat(response.body().jsonPath().getBoolean("is_hidden")).isEqualTo(menu.isHidden()); - - softly.assertThat(response.body().jsonPath().getBoolean("is_single")).isFalse(); - softly.assertThat((Integer)response.body().jsonPath().get("single_price")).isNull(); - softly.assertThat(response.body().jsonPath().getList("option_prices")).hasSize(2); - softly.assertThat(response.body().jsonPath().getString("description")).isEqualTo(menu.getDescription()); - softly.assertThat(response.body().jsonPath().getList("category_ids")) - .hasSize(menu.getMenuCategoryMaps().size()); - softly.assertThat(response.body().jsonPath().getList("image_urls")) - .hasSize(menu.getMenuImages().size()); - } - ); + mockMvc.perform( + get("/owner/shops/menus/{menuId}", menu.getId()) + .header("Authorization", "Bearer " + token_현수) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(menu.getId())) + .andExpect(jsonPath("$.shop_id").value(menu.getShopId())) + .andExpect(jsonPath("$.name").value(menu.getName())) + .andExpect(jsonPath("$.is_hidden").value(menu.isHidden())) + .andExpect(jsonPath("$.is_single").value(false)) + .andExpect(jsonPath("$.single_price").doesNotExist()) + .andExpect(jsonPath("$.option_prices", hasSize(2))) + .andExpect(jsonPath("$.description").value(menu.getDescription())) + .andExpect(jsonPath("$.category_ids", hasSize(menu.getMenuCategoryMaps().size()))) + .andExpect(jsonPath("$.image_urls", hasSize(menu.getMenuImages().size()))); } @Test - @DisplayName("권한이 없는 상점 사장님이 특정 상점 조회") - void ownerCannotQueryOtherStoresWithoutPermission() { + void 권한이_없는_상점_사장님이_특정_상점_조회() throws Exception { // given - RestAssured - .given() - .header("Authorization", "Bearer " + token_준영) - .when() - .get("/owner/shops/{shopId}", shop_마슬랜.getId()) - .then() - .statusCode(HttpStatus.FORBIDDEN.value()) - .extract(); + mockMvc.perform( + get("/owner/shops/{shopId}", shop_마슬랜.getId()) + .header("Authorization", "Bearer " + token_준영) + ) + .andExpect(status().isForbidden()); } @Test - @DisplayName("사장님이 메뉴 카테고리를 삭제한다.") - void deleteMenuCategory() { + void 사장님이_메뉴_카테고리를_삭제한다() throws Exception { // when & then - RestAssured - .given() - .header("Authorization", "Bearer " + token_현수) - .when() - .delete("/owner/shops/menus/categories/{categoryId}", menuCategory_메인.getId()) - .then() - .statusCode(HttpStatus.NO_CONTENT.value()) - .extract(); - + mockMvc.perform( + delete("/owner/shops/menus/categories/{categoryId}", menuCategory_메인.getId()) + .header("Authorization", "Bearer " + token_현수) + ) + .andExpect(status().isNoContent()); assertThat(menuCategoryRepository.findById(menuCategory_메인.getId())).isNotPresent(); } @Test - @DisplayName("사장님이 메뉴를 삭제한다.") - void deleteMenu() { + void 사장님이_메뉴를_삭제한다() throws Exception { // given Menu menu = menuFixture.짜장면_단일메뉴(shop_마슬랜, menuCategory_메인); - - RestAssured - .given() - .header("Authorization", "Bearer " + token_현수) - .when() - .delete("/owner/shops/menus/{menuId}", menu.getId()) - .then() - .statusCode(HttpStatus.NO_CONTENT.value()) - .extract(); - + mockMvc.perform( + delete("/owner/shops/menus/{menuId}", menu.getId()) + .header("Authorization", "Bearer " + token_현수) + ) + .andExpect(status().isNoContent()); assertThat(menuRepository.findById(menu.getId())).isNotPresent(); } @Test - @DisplayName("사장님이 옵션이 여러개인 메뉴를 추가한다.") - void createManyOptionMenu() { + void 사장님이_옵션이_여러개인_메뉴를_추가한다() throws Exception { // given MenuCategory menuCategory = menuCategory_메인; - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token_현수) - .contentType(ContentType.JSON) - .body(String.format(""" - { - "category_ids": [ - %s - ], - "description": "테스트메뉴입니다.", - "image_urls": [ - "https://test-image.com/짜장면.jpg" - ], - "is_single": false, - "name": "짜장면", - "option_prices": [ - { - "option": "중", - "price": 10000 - }, - { - "option": "소", - "price": 5000 - } - ] - } - """, menuCategory.getId())) - .when() - .post("/owner/shops/{id}/menus", shop_마슬랜.getId()) - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); - - transactionTemplate.executeWithoutResult(status -> { - Menu menu = menuRepository.getById(1); - assertSoftly( - softly -> { - List menuCategoryMaps = menu.getMenuCategoryMaps(); - List menuOptions = menu.getMenuOptions(); - List menuImages = menu.getMenuImages(); - softly.assertThat(menu.getDescription()).isEqualTo("테스트메뉴입니다."); - softly.assertThat(menu.getName()).isEqualTo("짜장면"); - softly.assertThat(menuImages.get(0).getImageUrl()).isEqualTo("https://test-image.com/짜장면.jpg"); - softly.assertThat(menuCategoryMaps.get(0).getMenuCategory().getId()).isEqualTo(1); - softly.assertThat(menuOptions).hasSize(2); - } - ); - }); + mockMvc.perform( + post("/owner/shops/{id}/menus", shop_마슬랜.getId()) + .header("Authorization", "Bearer " + token_현수) + .contentType(MediaType.APPLICATION_JSON) + .content(String.format(""" + { + "category_ids": [ + %s + ], + "description": "테스트메뉴입니다.", + "image_urls": [ + "https://test-image.com/짜장면.jpg" + ], + "is_single": false, + "name": "짜장면", + "option_prices": [ + { + "option": "중", + "price": 10000 + }, + { + "option": "소", + "price": 5000 + } + ] + } + """, menuCategory.getId())) + ) + .andExpect(status().isCreated()); + Menu menu = menuRepository.getById(1); + assertSoftly( + softly -> { + List menuCategoryMaps = menu.getMenuCategoryMaps(); + List menuOptions = menu.getMenuOptions(); + List menuImages = menu.getMenuImages(); + softly.assertThat(menu.getDescription()).isEqualTo("테스트메뉴입니다."); + softly.assertThat(menu.getName()).isEqualTo("짜장면"); + softly.assertThat(menuImages.get(0).getImageUrl()).isEqualTo("https://test-image.com/짜장면.jpg"); + softly.assertThat(menuCategoryMaps.get(0).getMenuCategory().getId()).isEqualTo(1); + softly.assertThat(menuOptions).hasSize(2); + } + ); } @Test - @DisplayName("사장님이 옵션이 한개인 메뉴를 추가한다.") - void createOneOptionMenu() { + void 사장님이_옵션이_한개인_메뉴를_추가한다() throws Exception { // given MenuCategory menuCategory = menuCategory_메인; - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token_현수) - .contentType(ContentType.JSON) - .body(String.format(""" - { - "category_ids": [ - %s - ], - "description": "테스트메뉴입니다.", - "image_urls": [ - "https://test-image.com/짜장면.jpg" - ], - "is_single": true, - "name": "짜장면", - "option_prices": null, - "single_price": 10000 - } - """, menuCategory.getId())) - .when() - .post("/owner/shops/{id}/menus", shop_마슬랜.getId()) - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); - - transactionTemplate.executeWithoutResult(status -> { + mockMvc.perform( + post("/owner/shops/{id}/menus", shop_마슬랜.getId()) + .header("Authorization", "Bearer " + token_현수) + .content(String.format(""" + { + "category_ids": [ + %s + ], + "description": "테스트메뉴입니다.", + "image_urls": [ + "https://test-image.com/짜장면.jpg" + ], + "is_single": true, + "name": "짜장면", + "option_prices": null, + "single_price": 10000 + } + """, menuCategory.getId())) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isCreated()); + transactionTemplate.executeWithoutResult(execute -> { Menu menu = menuRepository.getById(1); assertSoftly( softly -> { @@ -567,10 +493,8 @@ void createOneOptionMenu() { List menuImages = menu.getMenuImages(); softly.assertThat(menu.getDescription()).isEqualTo("테스트메뉴입니다."); softly.assertThat(menu.getName()).isEqualTo("짜장면"); - softly.assertThat(menuImages.get(0).getImageUrl()).isEqualTo("https://test-image.com/짜장면.jpg"); softly.assertThat(menuCategoryMaps.get(0).getMenuCategory().getId()).isEqualTo(1); - softly.assertThat(menuOptions.get(0).getPrice()).isEqualTo(10000); } ); @@ -578,58 +502,48 @@ void createOneOptionMenu() { } @Test - @DisplayName("사장님이 메뉴 카테고리를 추가한다.") - void createMenuCategory() { - // given - RestAssured - .given() - .header("Authorization", "Bearer " + token_현수) - .contentType(ContentType.JSON) - .body(String.format(""" - { - "name": "대박메뉴" - } - """)) - .when() - .post("/owner/shops/{id}/menus/categories", shop_마슬랜.getId()) - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); - + void 사장님이_메뉴_카테고리를_추가한다() throws Exception { + menuFixture.짜장면_단일메뉴(shop_마슬랜, menuCategory_메인); + mockMvc.perform( + post("/owner/shops/{id}/menus/categories", shop_마슬랜.getId()) + .header("Authorization", "Bearer " + token_현수) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "name": "대박메뉴" + } + """) + ) + .andExpect(status().isCreated()); var menuCategories = menuCategoryRepository.findAllByShopId(shop_마슬랜.getId()); - assertThat(menuCategories).anyMatch(menuCategory -> "대박메뉴".equals(menuCategory.getName())); } @Test - @DisplayName("사장님이 단일 메뉴로 수정한다.") - void modifyOneMenu() { + void 사장님이_단일_메뉴로_수정한다() throws Exception { // given Menu menu = menuFixture.짜장면_단일메뉴(shop_마슬랜, menuCategory_메인); - RestAssured - .given() - .header("Authorization", "Bearer " + token_현수) - .contentType(ContentType.JSON) - .body(String.format(""" - { - "category_ids": [ - %d - ], - "description": "테스트메뉴수정", - "image_urls": [ - "https://test-image.net/테스트메뉴.jpeg" - ], - "is_single": true, - "name": "짜장면2", - "single_price": 10000 - } - """, shopCategory_일반.getId())) - .when() - .put("/owner/shops/menus/{menuId}", menu.getId()) - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); + mockMvc.perform( + put("/owner/shops/menus/{menuId}", menu.getId()) + .header("Authorization", "Bearer " + token_현수) + .contentType(MediaType.APPLICATION_JSON) + .content(String.format(""" + { + "category_ids": [ + %d + ], + "description": "테스트메뉴수정", + "image_urls": [ + "https://test-image.net/테스트메뉴.jpeg" + ], + "is_single": true, + "name": "짜장면2", + "single_price": 10000 + } + """, shopCategory_일반.getId())) + ) + .andExpect(status().isCreated()); transactionTemplate.executeWithoutResult(status -> { Menu result = menuRepository.getById(1); @@ -640,55 +554,49 @@ void modifyOneMenu() { List menuImages = result.getMenuImages(); softly.assertThat(result.getDescription()).isEqualTo("테스트메뉴수정"); softly.assertThat(result.getName()).isEqualTo("짜장면2"); - softly.assertThat(menuImages.get(0).getImageUrl()).isEqualTo("https://test-image.net/테스트메뉴.jpeg"); softly.assertThat(menuCategoryMaps.get(0).getMenuCategory().getId()).isEqualTo(2); - softly.assertThat(menuOptions.get(0).getPrice()).isEqualTo(10000); - } ); }); } @Test - @DisplayName("사장님이 여러옵션을 가진 메뉴로 수정한다.") - void modifyManyOptionMenu() { + void 사장님이_여러옵션을_가진_메뉴로_수정한다() throws Exception { // given Menu menu = menuFixture.짜장면_옵션메뉴(shop_마슬랜, menuCategory_메인); - RestAssured - .given() - .header("Authorization", "Bearer " + token_현수) - .contentType(ContentType.JSON) - .body(String.format(""" - { - "category_ids": [ - %d, %d - ], - "description": "테스트메뉴입니다.", - "image_urls": [ - "https://fixed-testimage.com/수정된짜장면.png" - ], - "is_single": false, - "name": "짜장면", - "option_prices": [ - { - "option": "중", - "price": 10000 - }, - { - "option": "소", - "price": 5000 - } - ] - } - """, menuCategory_메인.getId(), menuCategory_사이드.getId()) + + mockMvc.perform( + put("/owner/shops/menus/{menuId}", menu.getId()) + .header("Authorization", "Bearer " + token_현수) + .contentType(MediaType.APPLICATION_JSON) + .content(String.format(""" + { + "category_ids": [ + %d, %d + ], + "description": "테스트메뉴입니다.", + "image_urls": [ + "https://fixed-testimage.com/수정된짜장면.png" + ], + "is_single": false, + "name": "짜장면", + "option_prices": [ + { + "option": "중", + "price": 10000 + }, + { + "option": "소", + "price": 5000 + } + ] + } + """, menuCategory_메인.getId(), menuCategory_사이드.getId()) + ) ) - .when() - .put("/owner/shops/menus/{menuId}", menu.getId()) - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); + .andExpect(status().isCreated()); transactionTemplate.executeWithoutResult(status -> { Menu result = menuRepository.getById(1); @@ -709,51 +617,48 @@ void modifyManyOptionMenu() { } @Test - @DisplayName("사장님이 상점을 수정한다.") - void modifyShop() { + void 사장님이_상점을_수정한다() throws Exception { // given - RestAssured - .given() - .header("Authorization", "Bearer " + token_현수) - .contentType(ContentType.JSON) - .body(String.format(""" - { - "address": "충청남도 천안시 동남구 병천면 충절로 1600", - "category_ids": [ - %d, %d - ], - "delivery": false, - "delivery_price": 1000, - "description": "이번주 전 메뉴 10%% 할인 이벤트합니다.", - "image_urls": [ - "https://fixed-shopimage.com/수정된_상점_이미지.png" - ], - "name": "써니 숯불 도시락", - "open": [ - { - "close_time": "22:30", - "closed": false, - "day_of_week": "MONDAY", - "open_time": "10:00" - }, - { - "close_time": "23:30", - "closed": true, - "day_of_week": "SUNDAY", - "open_time": "11:00" - } - ], - "pay_bank": true, - "pay_card": true, - "phone": "041-123-4567" - } - """, shopCategory_일반.getId(), shopCategory_치킨.getId() - )) - .when() - .put("/owner/shops/{id}", shop_마슬랜.getId()) - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); + mockMvc.perform( + put("/owner/shops/{id}", shop_마슬랜.getId()) + .header("Authorization", "Bearer " + token_현수) + .contentType(MediaType.APPLICATION_JSON) + .content(String.format(""" + { + "address": "충청남도 천안시 동남구 병천면 충절로 1600", + "category_ids": [ + %d, %d + ], + "delivery": false, + "delivery_price": 1000, + "description": "이번주 전 메뉴 10%% 할인 이벤트합니다.", + "image_urls": [ + "https://fixed-shopimage.com/수정된_상점_이미지.png" + ], + "name": "써니 숯불 도시락", + "open": [ + { + "close_time": "22:30", + "closed": false, + "day_of_week": "MONDAY", + "open_time": "10:00" + }, + { + "close_time": "23:30", + "closed": true, + "day_of_week": "SUNDAY", + "open_time": "11:00" + } + ], + "pay_bank": true, + "pay_card": true, + "phone": "041-123-4567" + } + """, shopCategory_일반.getId(), shopCategory_치킨.getId() + ) + ) + ) + .andExpect(status().isCreated()); transactionTemplate.executeWithoutResult(status -> { Shop result = shopRepository.getById(1); @@ -793,337 +698,294 @@ void modifyShop() { } @Test - @DisplayName("사장님이 단일 메뉴로 수정한다. - 가격 옵션이 null이 아니면 400 에러") - void modifySingleMenuWithEmptyOptionPrices() { + void 사장님이_단일_메뉴로_수정한다_가격_옵션이_null이_아니면_400_에러() throws Exception { // given Menu menu = menuFixture.짜장면_단일메뉴(shop_마슬랜, menuCategory_메인); - RestAssured - .given() - .header("Authorization", "Bearer " + token_현수) - .contentType(ContentType.JSON) - .body(String.format(""" - { - "category_ids": [ - %d - ], - "description": "테스트메뉴수정", - "image_urls": [ - "https://test-image.net/테스트메뉴.jpeg" - ], - "is_single": true, - "name": "짜장면2", - "single_price": 10000, - "option_prices": [] - } - """, shopCategory_일반.getId())) - .when() - .put("/owner/shops/menus/{menuId}", menu.getId()) - .then() - .statusCode(HttpStatus.BAD_REQUEST.value()) - .extract(); + mockMvc.perform( + put("/owner/shops/menus/{menuId}", menu.getId()) + .header("Authorization", "Bearer " + token_현수) + .contentType(MediaType.APPLICATION_JSON) + .content(String.format(""" + { + "category_ids": [ + %d + ], + "description": "테스트메뉴수정", + "image_urls": [ + "https://test-image.net/테스트메뉴.jpeg" + ], + "is_single": true, + "name": "짜장면2", + "single_price": 10000, + "option_prices": [] + } + """, shopCategory_일반.getId()) + ) + ) + .andExpect(status().isBadRequest()); } - @Test - @DisplayName("권한이 없는 상점 사장님이 특정 카테고리 조회한다.") - void ownerCannotQueryOtherCategoriesWithoutPermission() { + void 권한이_없는_상점_사장님이_특정_카테고리_조회한다() throws Exception { // given - RestAssured - .given() - .param("shopId", 1) - .header("Authorization", "Bearer " + token_준영) - .when() - .get("/owner/shops/menus/categories") - .then() - .statusCode(HttpStatus.FORBIDDEN.value()) - .extract(); + mockMvc.perform( + get("/owner/shops/menus/categories") + .header("Authorization", "Bearer " + token_준영) + .param("shopId", shop_마슬랜.getId().toString()) + ) + .andExpect(status().isForbidden()); } @Test - @DisplayName("권한이 없는 상점 사장님이 특정 메뉴 조회한다.") - void ownerCannotQueryOtherMenusWithoutPermission() { + void 권한이_없는_상점_사장님이_특정_메뉴_조회한다() throws Exception { + menuFixture.짜장면_단일메뉴(shop_마슬랜, menuCategory_메인); // given - RestAssured - .given() - .header("Authorization", "Bearer " + token_준영) - .param("shopId", 1) - .when() - .get("/owner/shops/menus") - .then() - .statusCode(HttpStatus.FORBIDDEN.value()) - .extract(); + mockMvc.perform( + get("/owner/shops/menus/{menuId}", 1) + .header("Authorization", "Bearer " + token_준영) + ) + .andExpect(status().isForbidden()); } @Test - @DisplayName("권한이 없는 사장님이 메뉴 카테고리를 삭제한다.") - void ownerCannotDeleteOtherCategoriesWithoutPermission() { + void 권한이_없는_사장님이_메뉴_카테고리를_삭제한다() throws Exception { // given MenuCategory menuCategory = menuCategoryFixture.세트메뉴(shop_마슬랜); - RestAssured - .given() - .header("Authorization", "Bearer " + token_준영) - .when() - .delete("/owner/shops/menus/categories/{categoryId}", menuCategory.getId()) - .then() - .statusCode(HttpStatus.FORBIDDEN.value()) - .extract(); + mockMvc.perform( + delete("/owner/shops/menus/categories/{categoryId}", menuCategory.getId()) + .header("Authorization", "Bearer " + token_준영) + ) + .andExpect(status().isForbidden()); } @Test - @DisplayName("권한이 없는 사장님이 메뉴를 삭제한다.") - void ownerCannotDeleteOtherMenusWithoutPermission() { + void 권한이_없는_사장님이_메뉴를_삭제한다() throws Exception { // given Menu menu = menuFixture.짜장면_단일메뉴(shop_마슬랜, menuCategory_메인); - - RestAssured - .given() - .header("Authorization", "Bearer " + token_준영) - .when() - .delete("/owner/shops/menus/{menuId}", menu.getId()) - .then() - .statusCode(HttpStatus.FORBIDDEN.value()) - .extract(); + mockMvc.perform( + delete("/owner/shops/menus/{menuId}", menu.getId()) + .header("Authorization", "Bearer " + token_준영) + ) + .andExpect(status().isForbidden()); } @Test - @DisplayName("사장님이 이벤트를 추가한다.") - void ownerShopCreateEvent() { + void 사장님이_이벤트를_추가한다() throws Exception { LocalDate startDate = LocalDate.now(); LocalDate endDate = startDate.plusDays(10); - RestAssured - .given() - .header("Authorization", "Bearer " + token_현수) - .contentType(ContentType.JSON) - .body(String.format(""" - { - "title": "감성떡볶이 이벤트합니다!", - "content": "테스트 이벤트입니다.", - "thumbnail_images": [ - "https://test.com/test1.jpg" - ], - "start_date": "%s", - "end_date": "%s" - } - """, - startDate.format(ofPattern("yyyy-MM-dd")), - endDate.format(ofPattern("yyyy-MM-dd")) - )) - .when() - .post("/owner/shops/{shopId}/event", shop_마슬랜.getId()) - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); - transactionTemplate.executeWithoutResult(status -> { - EventArticle eventArticle = eventArticleRepository.getById(1); - assertSoftly( - softly -> { - softly.assertThat(eventArticle.getShop().getId()).isEqualTo(1); - softly.assertThat(eventArticle.getTitle()).isEqualTo("감성떡볶이 이벤트합니다!"); - softly.assertThat(eventArticle.getContent()).isEqualTo("테스트 이벤트입니다."); - softly.assertThat(eventArticle.getThumbnailImages().get(0).getThumbnailImage()) - .isEqualTo("https://test.com/test1.jpg"); - softly.assertThat(eventArticle.getStartDate()).isEqualTo(startDate); - softly.assertThat(eventArticle.getEndDate()).isEqualTo(endDate); - } - ); + try { + mockMvc.perform( + post("/owner/shops/{shopId}/event", shop_마슬랜.getId()) + .header("Authorization", "Bearer " + token_현수) + .contentType(MediaType.APPLICATION_JSON) + .content(String.format(""" + { + "title": "감성떡볶이 이벤트합니다!", + "content": "테스트 이벤트입니다.", + "thumbnail_images": [ + "https://test.com/test1.jpg" + ], + "start_date": "%s", + "end_date": "%s" + } + """, + startDate.format(ofPattern("yyyy-MM-dd")), + endDate.format(ofPattern("yyyy-MM-dd")) + ) + ) + ) + .andExpect(status().isCreated()); + } catch (Exception e) { + throw new RuntimeException(e); + } }); - verify(shopEventListener).onShopEventCreate(any()); + EventArticle eventArticle = eventArticleRepository.getById(1); + assertSoftly( + softly -> { + softly.assertThat(eventArticle.getShop().getId()).isEqualTo(1); + softly.assertThat(eventArticle.getTitle()).isEqualTo("감성떡볶이 이벤트합니다!"); + softly.assertThat(eventArticle.getContent()).isEqualTo("테스트 이벤트입니다."); + softly.assertThat(eventArticle.getThumbnailImages().get(0).getThumbnailImage()) + .isEqualTo("https://test.com/test1.jpg"); + softly.assertThat(eventArticle.getStartDate()).isEqualTo(startDate); + softly.assertThat(eventArticle.getEndDate()).isEqualTo(endDate); + } + ); + forceVerify(() -> verify(shopEventListener, times(1)).onShopEventCreate(any())); + clear(); + setUp(); } @Test - @DisplayName("사장님이 이벤트를 수정한다.") - void ownerShopModifyEvent() { + void 사장님이_이벤트를_수정한다() throws Exception { LocalDate startDate = LocalDate.now(); LocalDate endDate = startDate.plusDays(10); EventArticle eventArticle = eventArticleFixture.할인_이벤트(shop_마슬랜, startDate, endDate); - RestAssured - .given() - .header("Authorization", "Bearer " + token_현수) - .contentType(ContentType.JSON) - .body(String.format(""" - { - "title": "감성떡볶이 이벤트합니다!", - "content": "테스트 이벤트입니다.", - "thumbnail_images": [ - "https://test.com/test1.jpg" - ], - "start_date": "%s", - "end_date": "%s" - } - """, - startDate, - endDate)) - .when() - .put("/owner/shops/{shopId}/events/{eventId}", shop_마슬랜.getId(), eventArticle.getId()) - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); + mockMvc.perform( + put("/owner/shops/{shopId}/events/{eventId}", shop_마슬랜.getId(), eventArticle.getId()) + .header("Authorization", "Bearer " + token_현수) + .contentType(MediaType.APPLICATION_JSON) + .content(String.format(""" + { + "title": "감성떡볶이 이벤트합니다!", + "content": "테스트 이벤트입니다.", + "thumbnail_images": [ + "https://test.com/test1.jpg" + ], + "start_date": "%s", + "end_date": "%s" + } + """, + startDate, + endDate) + ) + ) + .andExpect(status().isCreated()); - transactionTemplate.executeWithoutResult(status -> { - EventArticle result = eventArticleRepository.getById(eventArticle.getId()); - assertSoftly( - softly -> { - softly.assertThat(result.getShop().getId()).isEqualTo(1); - softly.assertThat(result.getTitle()).isEqualTo("감성떡볶이 이벤트합니다!"); - softly.assertThat(result.getContent()).isEqualTo("테스트 이벤트입니다."); - softly.assertThat(result.getThumbnailImages().get(0).getThumbnailImage()) - .isEqualTo("https://test.com/test1.jpg"); - softly.assertThat(result.getStartDate()).isEqualTo(startDate); - softly.assertThat(result.getEndDate()).isEqualTo(endDate); - } - ); - }); + EventArticle result = eventArticleRepository.getById(eventArticle.getId()); + assertSoftly( + softly -> { + softly.assertThat(result.getShop().getId()).isEqualTo(1); + softly.assertThat(result.getTitle()).isEqualTo("감성떡볶이 이벤트합니다!"); + softly.assertThat(result.getContent()).isEqualTo("테스트 이벤트입니다."); + softly.assertThat(result.getThumbnailImages().get(0).getThumbnailImage()) + .isEqualTo("https://test.com/test1.jpg"); + softly.assertThat(result.getStartDate()).isEqualTo(startDate); + softly.assertThat(result.getEndDate()).isEqualTo(endDate); + } + ); } @Test - @DisplayName("사장님이 이벤트를 삭제한다.") - void ownerShopDeleteEvent() { + void 사장님이_이벤트를_삭제한다() throws Exception { EventArticle eventArticle = eventArticleFixture.할인_이벤트( shop_마슬랜, LocalDate.of(2024, 10, 24), LocalDate.of(2024, 10, 26) ); - - RestAssured - .given() - .header("Authorization", "Bearer " + token_현수) - .contentType(ContentType.JSON) - .when() - .delete("/owner/shops/{shopId}/events/{eventId}", shop_마슬랜.getId(), eventArticle.getId()) - .then() - .statusCode(HttpStatus.NO_CONTENT.value()) - .extract(); - + mockMvc.perform( + delete("/owner/shops/{shopId}/events/{eventId}", shop_마슬랜.getId(), eventArticle.getId()) + .header("Authorization", "Bearer " + token_현수) + ) + .andExpect(status().isNoContent()); Optional modifiedEventArticle = eventArticleRepository.findById(eventArticle.getId()); assertThat(modifiedEventArticle).isNotPresent(); } @Test - void 이미지_url의_요소가_공백인_채로_상점을_수정하면_400에러가_반환된다() { + void 이미지_url의_요소가_공백인_채로_상점을_수정하면_400에러가_반환된다() throws Exception { // given - RestAssured - .given() - .header("Authorization", "Bearer " + token_현수) - .contentType(ContentType.JSON) - .body(String.format(""" - { - "address": "충청남도 천안시 동남구 병천면 충절로 1600", - "category_ids": [ - %d, %d - ], - "delivery": false, - "delivery_price": 1000, - "description": "이번주 전 메뉴 10%% 할인 이벤트합니다.", - "image_urls": [ - "" - ], - "name": "써니 숯불 도시락", - "open": [ - { - "close_time": "22:30", - "closed": false, - "day_of_week": "MONDAY", - "open_time": "10:00" - }, - { - "close_time": "23:30", - "closed": true, - "day_of_week": "SUNDAY", - "open_time": "11:00" - } - ], - "pay_bank": true, - "pay_card": true, - "phone": "041-123-4567" - } - """, shopCategory_일반.getId(), shopCategory_치킨.getId() - )) - .when() - .put("/owner/shops/{id}", shop_마슬랜.getId()) - .then() - .statusCode(HttpStatus.BAD_REQUEST.value()) - .extract(); - + mockMvc.perform( + put("/owner/shops/{shopId}", shop_마슬랜.getId()) + .header("Authorization", "Bearer " + token_현수) + .contentType(MediaType.APPLICATION_JSON) + .content(String.format(""" + { + "address": "충청남도 천안시 동남구 병천면 충절로 1600", + "category_ids": [ + %d, %d + ], + "delivery": false, + "delivery_price": 1000, + "description": "이번주 전 메뉴 10%% 할인 이벤트합니다.", + "image_urls": [ + "" + ], + "name": "써니 숯불 도시락", + "open": [ + { + "close_time": "22:30", + "closed": false, + "day_of_week": "MONDAY", + "open_time": "10:00" + }, + { + "close_time": "23:30", + "closed": true, + "day_of_week": "SUNDAY", + "open_time": "11:00" + } + ], + "pay_bank": true, + "pay_card": true, + "phone": "041-123-4567" + } + """, shopCategory_일반.getId(), shopCategory_치킨.getId() + ))) + .andExpect(status().isBadRequest()); } @Test - void 이미지_url의_요소가_공백인_채로_상점을_생성하면_400에러가_반환된다() { + void 이미지_url의_요소가_공백인_채로_상점을_생성하면_400에러가_반환된다() throws Exception { // given - RestAssured - .given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + token_현수) - .body(String.format(""" - { - "address": "대전광역시 유성구 대학로 291", - "category_ids": [ - %d - ], - "delivery": true, - "delivery_price": 4000, - "description": "테스트 상점2입니다.", - "image_urls": [ - "", - ], - - "name": "테스트 상점2", - "open": [ - { - "close_time": "21:00", - "closed": false, - "day_of_week": "MONDAY", - "open_time": "09:00" - }, - { - "close_time": "21:00", - "closed": false, - "day_of_week": "TUESDAY", - "open_time": "09:00" - }, - { - "close_time": "21:00", - "closed": false, - "day_of_week": "WEDNESDAY", - "open_time": "09:00" - }, - { - "close_time": "21:00", - "closed": false, - "day_of_week": "THURSDAY", - "open_time": "09:00" - }, - { - "close_time": "21:00", - "closed": false, - "day_of_week": "FRIDAY", - "open_time": "09:00" - }, - { - "close_time": "21:00", - "closed": false, - "day_of_week": "SATURDAY", - "open_time": "09:00" - }, - { - "close_time": "21:00", - "closed": false, - "day_of_week": "SUNDAY", - "open_time": "09:00" - } - ], - "pay_bank": true, - "pay_card": true, - "phone": "010-1234-5678" - } - """, shopCategory_치킨.getId()) - ) - .when() - .post("/owner/shops") - .then() - .log().all() - .statusCode(HttpStatus.BAD_REQUEST.value()) - .extract(); + mockMvc.perform(post("/owner/shops") + .header("Authorization", "Bearer " + token_현수) + .contentType(MediaType.APPLICATION_JSON) + .content(String.format(""" + { + "address": "대전광역시 유성구 대학로 291", + "category_ids": [ + %d + ], + "delivery": true, + "delivery_price": 4000, + "description": "테스트 상점2입니다.", + "image_urls": [ + "", + ], + "name": "테스트 상점2", + "open": [ + { + "close_time": "21:00", + "closed": false, + "day_of_week": "MONDAY", + "open_time": "09:00" + }, + { + "close_time": "21:00", + "closed": false, + "day_of_week": "TUESDAY", + "open_time": "09:00" + }, + { + "close_time": "21:00", + "closed": false, + "day_of_week": "WEDNESDAY", + "open_time": "09:00" + }, + { + "close_time": "21:00", + "closed": false, + "day_of_week": "THURSDAY", + "open_time": "09:00" + }, + { + "close_time": "21:00", + "closed": false, + "day_of_week": "FRIDAY", + "open_time": "09:00" + }, + { + "close_time": "21:00", + "closed": false, + "day_of_week": "SATURDAY", + "open_time": "09:00" + }, + { + "close_time": "21:00", + "closed": false, + "day_of_week": "SUNDAY", + "open_time": "09:00" + } + ], + "pay_bank": true, + "pay_card": true, + "phone": "010-1234-5678" + } + """, shopCategory_치킨.getId()) + )) + .andExpect(status().isBadRequest()); } } diff --git a/src/test/java/in/koreatech/koin/acceptance/ShopApiTest.java b/src/test/java/in/koreatech/koin/acceptance/ShopApiTest.java index 95f79b5d3..ec4e2144a 100644 --- a/src/test/java/in/koreatech/koin/acceptance/ShopApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/ShopApiTest.java @@ -2,21 +2,20 @@ import static in.koreatech.koin.domain.shop.model.ReportStatus.DISMISSED; import static in.koreatech.koin.domain.shop.model.ReportStatus.UNHANDLED; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import java.time.LocalDate; -import java.time.LocalDateTime; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; +import org.springframework.transaction.annotation.Transactional; import in.koreatech.koin.AcceptanceTest; import in.koreatech.koin.domain.owner.model.Owner; import in.koreatech.koin.domain.shop.model.Menu; -import in.koreatech.koin.domain.shop.model.ReportStatus; import in.koreatech.koin.domain.shop.model.Shop; import in.koreatech.koin.domain.shop.model.ShopReview; import in.koreatech.koin.domain.user.model.Student; @@ -28,11 +27,10 @@ import in.koreatech.koin.fixture.ShopReviewFixture; import in.koreatech.koin.fixture.ShopReviewReportFixture; import in.koreatech.koin.fixture.UserFixture; -import in.koreatech.koin.support.JsonAssertions; -import io.restassured.RestAssured; -import io.restassured.http.ContentType; +@Transactional @SuppressWarnings("NonAsciiCharacters") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class ShopApiTest extends AcceptanceTest { @Autowired @@ -64,29 +62,22 @@ class ShopApiTest extends AcceptanceTest { private Student 익명_학생; - @BeforeEach + @BeforeAll void setUp() { + clear(); owner = userFixture.준영_사장님(); 마슬랜 = shopFixture.마슬랜(owner); 익명_학생 = userFixture.익명_학생(); } @Test - @DisplayName("옵션이 하나 있는 상점의 메뉴를 조회한다.") - void findMenuSingleOption() { - // given + void 옵션이_하나_있는_상점의_메뉴를_조회한다() throws Exception { Menu menu = menuFixture.짜장면_단일메뉴(마슬랜, menuCategoryFixture.메인메뉴(마슬랜)); - - var response = RestAssured - .given() - .when() - .get("/shops/{shopId}/menus/{menuId}", menu.getShopId(), menu.getId()) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/shops/{shopId}/menus/{menuId}", menu.getShopId(), menu.getId()) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "id": 1, "shop_id": 1, @@ -104,26 +95,19 @@ void findMenuSingleOption() { "https://test.com/짜장면22.jpg" ] } - """ + """) ); } @Test - @DisplayName("옵션이 여러 개 있는 상점의 메뉴를 조회한다.") - void findMenuMultipleOption() { - // given + void 옵션이_여러_개_있는_상점의_메뉴를_조회한다() throws Exception { Menu menu = menuFixture.짜장면_옵션메뉴(마슬랜, menuCategoryFixture.메인메뉴(마슬랜)); - var response = RestAssured - .given() - .when() - .get("/shops/{shopId}/menus/{menuId}", menu.getShopId(), menu.getId()) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/shops/{shopId}/menus/{menuId}", menu.getShopId(), menu.getId()) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "id": 1, "shop_id": 1, @@ -150,241 +134,178 @@ void findMenuMultipleOption() { "https://test.com/짜장면22.jpg" ] } - HTTP/1.1 200\s - Vary: Origin - Vary: Access-Control-Request-Method - Vary: Access-Control-Request-Headers - Content-Type: application/json - Transfer-Encoding: chunked - Date: Mon, 22 Apr 2024 15:59:58 GMT - Keep-Alive: timeout=60 - Connection: keep-alive - - { - "id": 1, - "shop_id": 1, - "name": "짜장면", - "is_hidden": false, - "is_single": false, - "single_price": null, - "option_prices": [ - { - "option": "곱빼기", - "price": 7500 - }, - { - "option": "일반", - "price": 7000 - } - ], - "description": "맛있는 짜장면", - "category_ids": [ - 1 - ], - "image_urls": [ - "https://test.com/짜장면.jpg", - "https://test.com/짜장면22.jpg" - ] - } - """); + """)); } @Test - @DisplayName("상점의 메뉴 카테고리들을 조회한다.") - void findShopMenuCategories() { - // given + void 상점의_메뉴_카테고리들을_조회한다() throws Exception { menuCategoryFixture.사이드메뉴(마슬랜); menuCategoryFixture.세트메뉴(마슬랜); Menu menu = menuFixture.짜장면_단일메뉴(마슬랜, menuCategoryFixture.추천메뉴(마슬랜)); - var response = RestAssured - .given() - .when() - .get("/shops/{shopId}/menus/categories", menu.getShopId()) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/shops/{shopId}/menus/categories", menu.getShopId()) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { - "count": 3, - "menu_categories": [ - { - "id": 3, - "name": "추천 메뉴" - }, - { - "id": 2, - "name": "세트 메뉴" - }, - { - "id": 1, - "name": "사이드 메뉴" - } - ] - } - """); + "count": 3, + "menu_categories": [ + { + "id": 3, + "name": "추천 메뉴" + }, + { + "id": 2, + "name": "세트 메뉴" + }, + { + "id": 1, + "name": "사이드 메뉴" + } + ] + } + """)); } @Test - @DisplayName("특정 상점 조회") - void getShop() { - // given + void 특정_상점_조회() throws Exception { menuCategoryFixture.사이드메뉴(마슬랜); menuCategoryFixture.세트메뉴(마슬랜); - var response = RestAssured - .given() - .when() - .get("/shops/{shopId}", 마슬랜.getId()) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" - { - "address": "천안시 동남구 병천면 1600", - "delivery": true, - "delivery_price": 3000, - "description": "마슬랜 치킨입니다.", - "id": 1, - "image_urls": [ - "https://test-image.com/마슬랜.png", - "https://test-image.com/마슬랜2.png" - ], - "menu_categories": [ - { - "id": 2, - "name": "세트 메뉴" - }, - { - "id": 1, - "name": "사이드 메뉴" - } - ], - "name": "마슬랜 치킨", - "open": [ - { - "day_of_week": "MONDAY", - "closed": false, - "open_time": "00:00", - "close_time": "21:00" - }, - { - "day_of_week": "FRIDAY", - "closed": false, - "open_time": "00:00", - "close_time": "00:00" - } - ], - "pay_bank": true, - "pay_card": true, - "phone": "010-7574-1212", - "shop_categories": [ - - ], - "updated_at": "2024-01-15", - "is_event": false, - "bank": "국민", - "account_number": "01022595923" - } - """ - ); + mockMvc.perform( + get("/shops/{shopId}", 마슬랜.getId()) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + + { + "address": "천안시 동남구 병천면 1600", + "delivery": true, + "delivery_price": 3000, + "description": "마슬랜 치킨입니다.", + "id": 1, + "image_urls": [ + "https://test-image.com/마슬랜.png", + "https://test-image.com/마슬랜2.png" + ], + "menu_categories": [ + { + "id": 2, + "name": "세트 메뉴" + }, + { + "id": 1, + "name": "사이드 메뉴" + } + ], + "name": "마슬랜 치킨", + "open": [ + { + "day_of_week": "MONDAY", + "closed": false, + "open_time": "00:00", + "close_time": "21:00" + }, + { + "day_of_week": "FRIDAY", + "closed": false, + "open_time": "00:00", + "close_time": "00:00" + } + ], + "pay_bank": true, + "pay_card": true, + "phone": "010-7574-1212", + "shop_categories": [ + \s + ], + "updated_at": "2024-01-15", + "is_event": false, + "bank": "국민", + "account_number": "01022595923" + } + """)); } @Test - @DisplayName("특정 상점 모든 메뉴 조회") - void getShopMenus() { + void 특정_상점_모든_메뉴_조회() throws Exception { menuFixture.짜장면_단일메뉴(마슬랜, menuCategoryFixture.추천메뉴(마슬랜)); menuFixture.짜장면_옵션메뉴(마슬랜, menuCategoryFixture.세트메뉴(마슬랜)); - var response = RestAssured - .given() - .when() - .get("/shops/{shopId}/menus", 마슬랜.getId()) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/shops/{id}/menus", 마슬랜.getId()) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { - "count": 2, - "menu_categories": [ - { - "id": 1, - "name": "추천 메뉴", - "menus": [ - { - "id": 1, - "name": "짜장면", - "is_hidden": false, - "is_single": true, - "single_price": 7000, - "option_prices": null, - "description": "맛있는 짜장면", - "image_urls": [ - "https://test.com/짜장면.jpg", - "https://test.com/짜장면22.jpg" - ] - } - ] - }, - { - "id": 2, - "name": "세트 메뉴", - "menus": [ - { - "id": 2, - "name": "짜장면", - "is_hidden": false, - "is_single": false, - "single_price": null, - "option_prices": [ - { - "option": "곱빼기", - "price": 7500 - }, - { - "option": "일반", - "price": 7000 - } - ], - "description": "맛있는 짜장면", - "image_urls": [ - "https://test.com/짜장면.jpg", - "https://test.com/짜장면22.jpg" - ] - } - ] - } - ], - "updated_at": "2024-01-15" - } - """ - ); + "count": 2, + "menu_categories": [ + { + "id": 1, + "name": "추천 메뉴", + "menus": [ + { + "id": 1, + "name": "짜장면", + "is_hidden": false, + "is_single": true, + "single_price": 7000, + "option_prices": null, + "description": "맛있는 짜장면", + "image_urls": [ + "https://test.com/짜장면.jpg", + "https://test.com/짜장면22.jpg" + ] + } + ] + }, + { + "id": 2, + "name": "세트 메뉴", + "menus": [ + { + "id": 2, + "name": "짜장면", + "is_hidden": false, + "is_single": false, + "single_price": null, + "option_prices": [ + { + "option": "곱빼기", + "price": 7500 + }, + { + "option": "일반", + "price": 7000 + } + ], + "description": "맛있는 짜장면", + "image_urls": [ + "https://test.com/짜장면.jpg", + "https://test.com/짜장면22.jpg" + ] + } + ] + } + ], + "updated_at": "2024-01-15" + } + """)); } @Test - @DisplayName("모든 상점 조회") - void getAllShop() { - // given + void 모든_상점_조회() throws Exception { shopFixture.영업중이_아닌_신전_떡볶이(owner); - var response = RestAssured - .given() - .when() - .get("/shops") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); // 2024-01-15 12:00 월요일 기준 boolean 마슬랜_영업여부 = true; boolean 신전_떡볶이_영업여부 = false; - System.out.println(LocalDateTime.now(clock)); - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + mockMvc.perform( + get("/shops") + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" { "count": 2, "shops": [ @@ -473,67 +394,47 @@ void getAllShop() { } ] } - """, 마슬랜_영업여부, 신전_떡볶이_영업여부)); + """, 마슬랜_영업여부, 신전_떡볶이_영업여부))); } @Test - @DisplayName("상점들의 모든 카테고리를 조회한다.") - void getAllShopCategories() { - // given + void 상점들의_모든_카테고리를_조회한다() throws Exception { shopCategoryFixture.카테고리_일반음식(); shopCategoryFixture.카테고리_치킨(); - var response = RestAssured - .given() - .when() - .get("/shops/categories") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/shops/categories") + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { - "total_count": 2, - "shop_categories": [ - { - "id": 1, - "image_url": "https://test-image.com/normal.jpg", - "name": "일반음식점" - }, - { - "id": 2, - "image_url": "https://test-image.com/ckicken.jpg", - "name": "치킨" - } - ] - } - """); + "total_count": 2, + "shop_categories": [ + { + "id": 1, + "image_url": "https://test-image.com/normal.jpg", + "name": "일반음식점" + }, + { + "id": 2, + "image_url": "https://test-image.com/ckicken.jpg", + "name": "치킨" + } + ] + } + """)); } @Test - @DisplayName("특정 상점의 이벤트들을 조회한다.") - void getShopEvents() { - eventArticleFixture.할인_이벤트( - 마슬랜, - LocalDate.now(clock).minusDays(3), - LocalDate.now(clock).plusDays(3) - ); - eventArticleFixture.참여_이벤트( - 마슬랜, - LocalDate.now(clock).minusDays(3), - LocalDate.now(clock).plusDays(3) - ); - - var response = RestAssured - .given() - .when() - .get("/shops/{shopId}/events", 마슬랜.getId()) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + void 특정_상점의_이벤트들을_조회한다() throws Exception { + eventArticleFixture.할인_이벤트(마슬랜, LocalDate.now(clock).minusDays(3), LocalDate.now(clock).plusDays(3)); + eventArticleFixture.참여_이벤트(마슬랜, LocalDate.now(clock).minusDays(3), LocalDate.now(clock).plusDays(3)); + + mockMvc.perform( + get("/shops/{shopId}/events", 마슬랜.getId()) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "events": [ { @@ -564,84 +465,51 @@ void getShopEvents() { } ] } - """); + """)); } @Test - @DisplayName("이벤트 진행중인 상점의 정보를 조회한다.") - void getShopWithEvents() { - eventArticleFixture.할인_이벤트( - 마슬랜, - LocalDate.now(clock).minusDays(3), - LocalDate.now(clock).plusDays(3) - ); - eventArticleFixture.참여_이벤트( - 마슬랜, - LocalDate.now(clock).minusDays(3), - LocalDate.now(clock).plusDays(3) - ); - - var response = RestAssured - .given() - .when() - .get("/shops/{shopId}", 마슬랜.getId()) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - Assertions.assertThat(response.jsonPath().getBoolean("is_event")).isTrue(); + void 이벤트_진행중인_상점의_정보를_조회한다() throws Exception { + eventArticleFixture.할인_이벤트(마슬랜, LocalDate.now(clock).minusDays(3), LocalDate.now(clock).plusDays(3)); + eventArticleFixture.참여_이벤트(마슬랜, LocalDate.now(clock).minusDays(3), LocalDate.now(clock).plusDays(3)); + + mockMvc.perform( + get("/shops/{shopId}", 마슬랜.getId()) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { + "is_event": true + } + """)); } @Test - @DisplayName("이벤트 진행중이지 않은 상점의 정보를 조회한다.") - void getShopWithoutEvents() { - eventArticleFixture.할인_이벤트( - 마슬랜, - LocalDate.now(clock).plusDays(3), - LocalDate.now(clock).plusDays(5) - ); - eventArticleFixture.참여_이벤트( - 마슬랜, - LocalDate.now(clock).minusDays(5), - LocalDate.now(clock).minusDays(3) - ); - - var response = RestAssured - .given() - .when() - .get("/shops/{shopId}", 마슬랜.getId()) - .then() - - .statusCode(HttpStatus.OK.value()) - .extract(); - - Assertions.assertThat(response.jsonPath().getBoolean("is_event")).isFalse(); + void 이벤트_진행중이지_않은_상점의_정보를_조회한다() throws Exception { + eventArticleFixture.할인_이벤트(마슬랜, LocalDate.now(clock).plusDays(3), LocalDate.now(clock).plusDays(5)); + eventArticleFixture.참여_이벤트(마슬랜, LocalDate.now(clock).minusDays(5), LocalDate.now(clock).minusDays(3)); + + mockMvc.perform( + get("/shops/{shopId}", 마슬랜.getId()) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { + "is_event": false + } + """)); } @Test - @DisplayName("이벤트 베너 조회") - void ownerShopDeleteEvent() { - eventArticleFixture.참여_이벤트( - 마슬랜, - LocalDate.now(clock), - LocalDate.now(clock).plusDays(10) - ); - eventArticleFixture.할인_이벤트( - 마슬랜, - LocalDate.now(clock).minusDays(10), - LocalDate.now(clock).minusDays(1) - ); - var response = RestAssured - .given() - .contentType(ContentType.JSON) - .when() - .get("/shops/events") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + void 이벤트_베너_조회() throws Exception { + eventArticleFixture.참여_이벤트(마슬랜, LocalDate.now(clock), LocalDate.now(clock).plusDays(10)); + eventArticleFixture.할인_이벤트(마슬랜, LocalDate.now(clock).minusDays(10), LocalDate.now(clock).minusDays(1)); + + mockMvc.perform( + get("/shops/events") + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "events": [ { @@ -659,30 +527,21 @@ void ownerShopDeleteEvent() { } ] } - """); + """)); } @Test - void 리뷰_평점순으로_정렬하여_모든_상점을_조회한다() { - // given + void 리뷰_평점순으로_정렬하여_모든_상점을_조회한다() throws Exception { Shop 영업중인_티바 = shopFixture.영업중인_티바(owner); shopReviewFixture.리뷰_4점(익명_학생, 영업중인_티바); - - var response = RestAssured - .given() - .queryParam("sorter", "RATING") - .when() - .get("/v2/shops") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - // 2024-01-15 12:00 월요일 기준 boolean 마슬랜_영업여부 = true; boolean 티바_영업여부 = true; - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + mockMvc.perform( + get("/v2/shops") + .queryParam("sorter", "RATING") + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" { "count": 2, "shops": [ @@ -775,33 +634,25 @@ void ownerShopDeleteEvent() { } ] } - """, 티바_영업여부, 마슬랜_영업여부)); + """, 티바_영업여부, 마슬랜_영업여부))); } @Test - void 리뷰_개수순으로_정렬하여_모든_상점을_조회한다() { - // given + void 리뷰_개수순으로_정렬하여_모든_상점을_조회한다() throws Exception { Shop 영업중인_티바 = shopFixture.영업중인_티바(owner); shopReviewFixture.리뷰_4점(익명_학생, 영업중인_티바); shopReviewFixture.리뷰_4점(익명_학생, 마슬랜); shopReviewFixture.리뷰_4점(익명_학생, 마슬랜); - - var response = RestAssured - .given() - .queryParam("sorter", "COUNT") - .when() - .get("/v2/shops") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - // 2024-01-15 12:00 월요일 기준 boolean 마슬랜_영업여부 = true; boolean 티바_영업여부 = true; - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + mockMvc.perform( + get("/v2/shops") + .queryParam("sorter", "COUNT") + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" { "count": 2, "shops": [ @@ -894,33 +745,25 @@ void ownerShopDeleteEvent() { } ] } - """, 티바_영업여부, 마슬랜_영업여부)); + """, 티바_영업여부, 마슬랜_영업여부))); } @Test - void 리뷰_개수가_많아도_영업중이_아니라면_정렬_우선순위가_낮은_상태로_모든_상점을_조회한다() { - // given + void 리뷰_개수가_많아도_영업중이_아니라면_정렬_우선순위가_낮은_상태로_모든_상점을_조회한다() throws Exception { Shop 영업중이_아닌_신전떡볶이 = shopFixture.영업중이_아닌_신전_떡볶이(owner); shopReviewFixture.리뷰_4점(익명_학생, 영업중이_아닌_신전떡볶이); shopReviewFixture.리뷰_4점(익명_학생, 영업중이_아닌_신전떡볶이); shopReviewFixture.리뷰_4점(익명_학생, 마슬랜); - - var response = RestAssured - .given() - .queryParam("sorter", "COUNT") - .when() - .get("/v2/shops") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - // 2024-01-15 12:00 월요일 기준 boolean 신전떡볶이_영업여부 = false; boolean 마슬랜_영업여부 = true; - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + mockMvc.perform( + get("/v2/shops") + .queryParam("sorter", "COUNT") + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" { "count": 2, "shops": [ @@ -1013,32 +856,26 @@ void ownerShopDeleteEvent() { } ] } - """, 마슬랜_영업여부, 신전떡볶이_영업여부)); + """, 마슬랜_영업여부, 신전떡볶이_영업여부))); } @Test - void 운영중인_상점만_필터하여_모든_상점을_조회한다() { - // given + void 운영중인_상점만_필터하여_모든_상점을_조회한다() throws Exception { Shop 영업중이_아닌_신전떡볶이 = shopFixture.영업중이_아닌_신전_떡볶이(owner); shopReviewFixture.리뷰_4점(익명_학생, 영업중이_아닌_신전떡볶이); shopReviewFixture.리뷰_4점(익명_학생, 영업중이_아닌_신전떡볶이); shopReviewFixture.리뷰_4점(익명_학생, 마슬랜); - var response = RestAssured - .given() - .queryParam("filter", "OPEN") - .when() - .get("/v2/shops") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - // 2024-01-15 12:00 월요일 기준 boolean 마슬랜_영업여부 = true; - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + mockMvc.perform( + get("/v2/shops") + .queryParam("filter", "OPEN") + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" { "count": 1, "shops": [ @@ -1073,32 +910,23 @@ void ownerShopDeleteEvent() { } ] } - """, 마슬랜_영업여부)); + """, 마슬랜_영업여부))); } @Test - void 배달_가능한_상점만_필터하여_모든_상점을_조회한다() { - // given + void 배달_가능한_상점만_필터하여_모든_상점을_조회한다() throws Exception { Shop 배달_안되는_신전_떡볶이 = shopFixture.배달_안되는_신전_떡볶이(owner); shopReviewFixture.리뷰_4점(익명_학생, 배달_안되는_신전_떡볶이); - shopReviewFixture.리뷰_4점(익명_학생, 배달_안되는_신전_떡볶이); shopReviewFixture.리뷰_4점(익명_학생, 마슬랜); - - var response = RestAssured - .given() - .queryParam("filter", "DELIVERY") - .when() - .get("/v2/shops") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - // 2024-01-15 12:00 월요일 기준 boolean 마슬랜_영업여부 = true; - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + mockMvc.perform( + get("/v2/shops") + .queryParam("filter", "DELIVERY") + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" { "count": 1, "shops": [ @@ -1133,12 +961,11 @@ void ownerShopDeleteEvent() { } ] } - """, 마슬랜_영업여부)); + """, 마슬랜_영업여부))); } @Test - void 배달_가능하고_영업중인_상점만_필터하여_모든_상점을_조회한다() { - // given + void 배달_가능하고_영업중인_상점만_필터하여_모든_상점을_조회한다() throws Exception { Shop 배달_안되는_신전_떡볶이 = shopFixture.배달_안되는_신전_떡볶이(owner); shopReviewFixture.리뷰_4점(익명_학생, 배달_안되는_신전_떡볶이); shopReviewFixture.리뷰_4점(익명_학생, 배달_안되는_신전_떡볶이); @@ -1146,22 +973,15 @@ void ownerShopDeleteEvent() { shopFixture.영업중이_아닌_신전_떡볶이(owner); shopReviewFixture.리뷰_4점(익명_학생, 마슬랜); - - var response = RestAssured - .given() - .queryParam("filter", "DELIVERY") - .queryParam("filter", "OPEN") - .when() - .get("/v2/shops") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - // 2024-01-15 12:00 월요일 기준 boolean 마슬랜_영업여부 = true; - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + mockMvc.perform( + get("/v2/shops") + .queryParam("filter", "DELIVERY") + .queryParam("filter", "OPEN") + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" { "count": 1, "shops": [ @@ -1196,12 +1016,11 @@ void ownerShopDeleteEvent() { } ] } - """, 마슬랜_영업여부)); + """, 마슬랜_영업여부))); } @Test - void 영업중인_상점만_필터하여_리뷰_개수_순으로_모든_상점을_조회한다() { - // given + void 영업중인_상점만_필터하여_리뷰_개수_순으로_모든_상점을_조회한다() throws Exception { Shop 배달_안되는_신전_떡볶이 = shopFixture.배달_안되는_신전_떡볶이(owner); shopReviewFixture.리뷰_4점(익명_학생, 배달_안되는_신전_떡볶이); shopReviewFixture.리뷰_4점(익명_학생, 배달_안되는_신전_떡볶이); @@ -1209,23 +1028,16 @@ void ownerShopDeleteEvent() { shopFixture.영업중이_아닌_신전_떡볶이(owner); shopReviewFixture.리뷰_4점(익명_학생, 마슬랜); - - var response = RestAssured - .given() - .queryParam("filter", "OPEN") - .queryParam("sorter", "COUNT") - .when() - .get("/v2/shops") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - // 2024-01-15 12:00 월요일 기준 boolean 신전_떡볶이_영업여부 = true; boolean 마슬랜_영업여부 = true; - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + mockMvc.perform( + get("/v2/shops") + .queryParam("filter", "OPEN") + .queryParam("sorter", "COUNT") + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" { "count": 2, "shops": [ @@ -1318,31 +1130,25 @@ void ownerShopDeleteEvent() { } ] } - """, 신전_떡볶이_영업여부, 마슬랜_영업여부)); + """, 신전_떡볶이_영업여부, 마슬랜_영업여부))); } @Test - void 신고된_리뷰의_내용도_반영해서_모든_상점을_조회한다() { - // given + void 신고된_리뷰의_내용도_반영해서_모든_상점을_조회한다() throws Exception { Shop 배달_안되는_신전_떡볶이 = shopFixture.배달_안되는_신전_떡볶이(owner); ShopReview 리뷰_4점 = shopReviewFixture.리뷰_4점(익명_학생, 배달_안되는_신전_떡볶이); shopReviewReportFixture.리뷰_신고(익명_학생, 리뷰_4점, UNHANDLED); shopReviewFixture.리뷰_4점(익명_학생, 마슬랜); - var response = RestAssured - .given() - .when() - .get("/v2/shops") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - // 2024-01-15 12:00 월요일 기준 boolean 신전_떡볶이_영업여부 = true; boolean 마슬랜_영업여부 = true; - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + mockMvc.perform( + get("/v2/shops") + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" { "count": 2, "shops": [ @@ -1435,31 +1241,24 @@ void ownerShopDeleteEvent() { } ] } - """, 마슬랜_영업여부, 신전_떡볶이_영업여부)); + """, 마슬랜_영업여부, 신전_떡볶이_영업여부))); } @Test - void 신고_반려된_리뷰는_반영된_상태로_모든_상점을_조회한다() { - // given + void 신고_반려된_리뷰는_반영된_상태로_모든_상점을_조회한다() throws Exception { Shop 배달_안되는_신전_떡볶이 = shopFixture.배달_안되는_신전_떡볶이(owner); ShopReview 리뷰_4점 = shopReviewFixture.리뷰_4점(익명_학생, 배달_안되는_신전_떡볶이); shopReviewReportFixture.리뷰_신고(익명_학생, 리뷰_4점, DISMISSED); shopReviewFixture.리뷰_4점(익명_학생, 마슬랜); - var response = RestAssured - .given() - .when() - .get("/v2/shops") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - // 2024-01-15 12:00 월요일 기준 boolean 신전_떡볶이_영업여부 = true; boolean 마슬랜_영업여부 = true; - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + mockMvc.perform( + get("/v2/shops") + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" { "count": 2, "shops": [ @@ -1552,6 +1351,7 @@ void ownerShopDeleteEvent() { } ] } - """, 마슬랜_영업여부, 신전_떡볶이_영업여부)); + """, 마슬랜_영업여부, 신전_떡볶이_영업여부))); } + } diff --git a/src/test/java/in/koreatech/koin/acceptance/ShopReviewApiTest.java b/src/test/java/in/koreatech/koin/acceptance/ShopReviewApiTest.java index c4e8b0d0e..7759e0e83 100644 --- a/src/test/java/in/koreatech/koin/acceptance/ShopReviewApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/ShopReviewApiTest.java @@ -1,18 +1,21 @@ package in.koreatech.koin.acceptance; -import static in.koreatech.koin.domain.shop.model.ReportStatus.DISMISSED; import static in.koreatech.koin.domain.shop.model.ReportStatus.UNHANDLED; import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import java.util.Optional; -import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; import in.koreatech.koin.AcceptanceTest; @@ -30,11 +33,10 @@ import in.koreatech.koin.fixture.ShopReviewReportCategoryFixture; import in.koreatech.koin.fixture.ShopReviewReportFixture; import in.koreatech.koin.fixture.UserFixture; -import in.koreatech.koin.support.JsonAssertions; -import io.restassured.RestAssured; -import io.restassured.http.ContentType; @SuppressWarnings("NonAsciiCharacters") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Transactional class ShopReviewApiTest extends AcceptanceTest { @Autowired @@ -78,8 +80,9 @@ class ShopReviewApiTest extends AcceptanceTest { private final int INITIAL_REVIEW_COUNT = 2; - @BeforeEach + @BeforeAll void setUp() { + clear(); 준호_학생 = userFixture.준호_학생(); 익명_학생 = userFixture.익명_학생(); 현수_사장님 = userFixture.현수_사장님(); @@ -95,12 +98,10 @@ void setUp() { @Test @DisplayName("사용자가 리뷰를 등록할 수 있다.") - void createReview() { - var response = RestAssured - .given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + token_준호) - .body(String.format(""" + void createReview() throws Exception { + mockMvc.perform(post("/shops/{shopId}/reviews", 신전_떡볶이.getId()) + .header("Authorization", "Bearer " + token_준호) + .content(String.format(""" { "rating": 4, "content": "정말 맛있어요~!", @@ -113,36 +114,16 @@ void createReview() { ] } """)) - .when() - .pathParam("shopId", 신전_떡볶이.getId()) - .post("/shops/{shopId}/reviews") - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); - - transactionTemplate.executeWithoutResult(status -> { - ShopReview shopReview = shopReviewRepository.getByIdAndIsDeleted(INITIAL_REVIEW_COUNT + 1); - assertSoftly( - softly -> { - softly.assertThat(shopReview.getRating()).isEqualTo(4); - softly.assertThat(shopReview.getContent()).isEqualTo("정말 맛있어요~!"); - softly.assertThat(shopReview.getImages().get(0).getImageUrls()) - .isEqualTo("https://static.koreatech.in/example.png"); - softly.assertThat(shopReview.getMenus().get(0).getMenuName()).isEqualTo("치킨"); - softly.assertThat(shopReview.getMenus().get(1).getMenuName()).isEqualTo("피자"); - verify(reviewEventListener).onReviewRegister(any()); - } - ); - }); + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isCreated()); } @Test - void 리뷰를_등록할_때_메뉴명을_공백으로_입력하면_예외가_발생한다() { - var response = RestAssured - .given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + token_준호) - .body(String.format(""" + void 리뷰를_등록할_때_메뉴명을_공백으로_입력하면_예외가_발생한다() throws Exception { + mockMvc.perform(post("/shops/{shopId}/reviews", 신전_떡볶이.getId()) + .header("Authorization", "Bearer " + token_준호) + .content(String.format(""" { "rating": 4, "content": "정말 맛있어요~!", @@ -154,21 +135,18 @@ void createReview() { ] } """)) - .when() - .pathParam("shopId", 신전_떡볶이.getId()) - .post("/shops/{shopId}/reviews") - .then() - .statusCode(HttpStatus.BAD_REQUEST.value()) - .extract(); + .contentType(MediaType.APPLICATION_JSON) + .content(""" + """) + ) + .andExpect(status().isBadRequest()); } @Test - void 리뷰_내용을_작성하지_않고_리뷰를_등록할_수_있다() { - var response = RestAssured - .given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + token_준호) - .body(String.format(""" + void 리뷰_내용을_작성하지_않고_리뷰를_등록할_수_있다() throws Exception { + mockMvc.perform(post("/shops/{shopId}/reviews", 신전_떡볶이.getId()) + .header("Authorization", "Bearer " + token_준호) + .content(String.format(""" { "rating": 4, "image_urls": [ @@ -180,38 +158,17 @@ void createReview() { ] } """)) - .when() - .pathParam("shopId", 신전_떡볶이.getId()) - .post("/shops/{shopId}/reviews") - .then() - .log().all() - .statusCode(HttpStatus.CREATED.value()) - .extract(); - - transactionTemplate.executeWithoutResult(status -> { - ShopReview shopReview = shopReviewRepository.getByIdAndIsDeleted(INITIAL_REVIEW_COUNT + 1); - assertSoftly( - softly -> { - softly.assertThat(shopReview.getRating()).isEqualTo(4); - softly.assertThat(shopReview.getContent()).isNull(); - softly.assertThat(shopReview.getImages().get(0).getImageUrls()) - .isEqualTo("https://static.koreatech.in/example.png"); - softly.assertThat(shopReview.getMenus().get(0).getMenuName()).isEqualTo("치킨"); - softly.assertThat(shopReview.getMenus().get(1).getMenuName()).isEqualTo("피자"); - verify(reviewEventListener).onReviewRegister(any()); - } - ); - }); + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isCreated()); } @Test @DisplayName("사용자가 본인의 리뷰를 수정할 수 있다.") - void modifyReview() { - var response = RestAssured - .given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + token_준호) - .body(String.format(""" + void modifyReview() throws Exception { + mockMvc.perform(put("/shops/{shopId}/reviews/{reviewId}", 신전_떡볶이.getId(), 준호_학생_리뷰.getId()) + .header("Authorization", "Bearer " + token_준호) + .content(String.format(""" { "rating": 3, "content": "정말 맛있어요!", @@ -223,46 +180,33 @@ void modifyReview() { ] } """)) - .when() - .pathParam("shopId", 신전_떡볶이.getId()) - .pathParam("reviewId", 준호_학생_리뷰.getId()) - .put("/shops/{shopId}/reviews/{reviewId}") - .then() - .statusCode(HttpStatus.NO_CONTENT.value()) - .extract(); + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isNoContent()); - transactionTemplate.executeWithoutResult(status -> { - ShopReview shopReview = shopReviewRepository.getByIdAndIsDeleted(준호_학생_리뷰.getId()); - assertSoftly( - softly -> { - softly.assertThat(shopReview.getRating()).isEqualTo(3); - softly.assertThat(shopReview.getContent()).isEqualTo("정말 맛있어요!"); - softly.assertThat(shopReview.getImages().get(0).getImageUrls()) - .isEqualTo("https://static.koreatech.in/example1.png"); - softly.assertThat(shopReview.getMenus().size()).isEqualTo(1); - } - ); - }); + ShopReview shopReview = shopReviewRepository.getByIdAndIsDeleted(준호_학생_리뷰.getId()); + assertSoftly( + softly -> { + softly.assertThat(shopReview.getRating()).isEqualTo(3); + softly.assertThat(shopReview.getContent()).isEqualTo("정말 맛있어요!"); + softly.assertThat(shopReview.getImages().get(0).getImageUrls()) + .isEqualTo("https://static.koreatech.in/example1.png"); + softly.assertThat(shopReview.getMenus().size()).isEqualTo(1); + } + ); } @Test @DisplayName("로그인한 사용자가 리뷰를 조회할 수 있다.") - void getReview() { - var response = RestAssured - .given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + token_준호) - .when() - .queryParam("limit", 10) - .queryParam("page", 1) - .pathParam("shopId", 준호_학생_리뷰.getShop().getId()) - .get("/shops/{shopId}/reviews") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + void getReview() throws Exception { + mockMvc.perform(get("/shops/{shopId}/reviews", 준호_학생_리뷰.getShop().getId()) + .header("Authorization", "Bearer " + token_준호) + .queryParam("limit", "10") + .queryParam("page", "1") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" { "total_count": 2, "current_count": 2, @@ -326,27 +270,20 @@ void getReview() { 익명_학생_리뷰.getContent(), 익명_학생_리뷰.getImages().get(0).getImageUrls(), 익명_학생_리뷰.getMenus().get(0).getMenuName()) - ); + )); } @Test - void 신고된_리뷰는_is_reported_가_true_이다() { + void 신고된_리뷰는_is_reported_가_true_이다() throws Exception { ShopReviewReport shopReviewReport = shopReviewReportFixture.리뷰_신고(준호_학생, 익명_학생_리뷰, UNHANDLED); - var response = RestAssured - .given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + token_준호) - .when() - .queryParam("limit", 10) - .queryParam("page", 1) - .pathParam("shopId", 신전_떡볶이.getId()) - .get("/shops/{shopId}/reviews") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + mockMvc.perform(get("/shops/{shopId}/reviews", 신전_떡볶이.getId()) + .header("Authorization", "Bearer " + token_준호) + .queryParam("limit", "10") + .queryParam("page", "1") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" { "total_count": 2, "current_count": 2, @@ -410,26 +347,19 @@ void getReview() { 익명_학생_리뷰.getContent(), 익명_학생_리뷰.getImages().get(0).getImageUrls(), 익명_학생_리뷰.getMenus().get(0).getMenuName()) - ); + )); } @Test @DisplayName("비회원이 리뷰를 조회할 수 있다.") - void getReviewByUnauthenticatedUser() { - var response = RestAssured - .given() - .contentType(ContentType.JSON) - .when() - .queryParam("limit", 10) - .queryParam("page", 1) - .pathParam("shopId", 신전_떡볶이.getId()) - .get("/shops/{shopId}/reviews") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + void getReviewByUnauthenticatedUser() throws Exception { + mockMvc.perform(get("/shops/{shopId}/reviews", 신전_떡볶이.getId()) + .queryParam("limit", "10") + .queryParam("page", "1") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" { "total_count": 2, "current_count": 2, @@ -493,23 +423,19 @@ void getReviewByUnauthenticatedUser() { 익명_학생_리뷰.getContent(), 익명_학생_리뷰.getImages().get(0).getImageUrls(), 익명_학생_리뷰.getMenus().get(0).getMenuName()) - ); + )); } @Test @DisplayName("리뷰 신고 카테고리를 조회할 수 있다.") - void getReviewReportCategories() { - var response = RestAssured - .given() - .contentType(ContentType.JSON) - .when() - .get("/shops/reviews/reports/categories") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + void getReviewReportCategories() throws Exception { + mockMvc.perform(get("/shops/reviews/reports/categories") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + """) + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" { "count": 4, "categories": [ @@ -540,18 +466,16 @@ void getReviewReportCategories() { 신고_카테고리_3.getDetail(), 신고_카테고리_4.getName(), 신고_카테고리_4.getDetail()) - ); + )); } @Test @DisplayName("특정 리뷰를 신고한다.") - void reportReview() { - - var response = RestAssured - .given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + token_준호) - .body(""" + void reportReview() throws Exception { + mockMvc.perform(post("/shops/{shopId}/reviews/{reviewId}/reports", 준호_학생_리뷰.getShop().getId(), 준호_학생_리뷰.getId()) + .header("Authorization", "Bearer " + token_준호) + .contentType(MediaType.APPLICATION_JSON) + .content(""" { "reports": [ { @@ -565,157 +489,66 @@ void reportReview() { ] } """) - .when() - .pathParam("shopId", 준호_학생_리뷰.getShop().getId()) - .pathParam("reviewId", 준호_학생_리뷰.getId()) - .post("/shops/{shopId}/reviews/{reviewId}/reports") - .then() - .statusCode(HttpStatus.NO_CONTENT.value()) - .extract(); + ) + .andExpect(status().isNoContent()); - transactionTemplate.executeWithoutResult(status -> { - Optional shopReviewReport1 = shopReviewReportRepository.findById(1); - Optional shopReviewReport2 = shopReviewReportRepository.findById(2); - assertSoftly( - softly -> { - softly.assertThat(shopReviewReport1.isPresent()).isTrue(); - softly.assertThat(shopReviewReport1.get().getTitle()).isEqualTo("기타"); - softly.assertThat(shopReviewReport1.get().getContent()).isEqualTo("적절치 못한 리뷰인 것 같습니다."); - softly.assertThat(shopReviewReport1.get().getReportStatus()).isEqualTo(UNHANDLED); - softly.assertThat(shopReviewReport2.get().getTitle()).isEqualTo("스팸"); - softly.assertThat(shopReviewReport2.get().getContent()).isEqualTo("광고가 포함된 리뷰입니다."); - softly.assertThat(shopReviewReport2.get().getReportStatus()).isEqualTo(UNHANDLED); - verify(reviewEventListener).onReviewReportRegister(any()); - } - ); - }); + assertSoftly( + softly -> { + Optional shopReviewReport1 = shopReviewReportRepository.findById(1); + Optional shopReviewReport2 = shopReviewReportRepository.findById(2); + softly.assertThat(shopReviewReport1.isPresent()).isTrue(); + softly.assertThat(shopReviewReport1.get().getTitle()).isEqualTo("기타"); + softly.assertThat(shopReviewReport1.get().getContent()).isEqualTo("적절치 못한 리뷰인 것 같습니다."); + softly.assertThat(shopReviewReport1.get().getReportStatus()).isEqualTo(UNHANDLED); + softly.assertThat(shopReviewReport2.get().getTitle()).isEqualTo("스팸"); + softly.assertThat(shopReviewReport2.get().getContent()).isEqualTo("광고가 포함된 리뷰입니다."); + softly.assertThat(shopReviewReport2.get().getReportStatus()).isEqualTo(UNHANDLED); + forceVerify(() -> verify(reviewEventListener).onReviewReportRegister(any())); + clear(); + setUp(); + } + ); } @Test @DisplayName("학생이 자신이 작성한 리뷰를 삭제한다.") - void deleteMyReview() { - var response = RestAssured - .given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + token_준호) - .when() - .pathParam("shopId", 준호_학생_리뷰.getShop().getId()) - .pathParam("reviewId", 준호_학생_리뷰.getId()) - .delete("/shops/{shopId}/reviews/{reviewId}") - .then() - .log().all() - .statusCode(HttpStatus.NO_CONTENT.value()) - .extract(); + void deleteMyReview() throws Exception { + mockMvc.perform(delete("/shops/{shopId}/reviews/{reviewId}", 준호_학생_리뷰.getShop().getId(), 준호_학생_리뷰.getId()) + .header("Authorization", "Bearer " + token_준호) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isNoContent()); - transactionTemplate.executeWithoutResult(status -> { - Optional shopReview = shopReviewRepository.findById(1); - assertSoftly( - softly -> { - softly.assertThat(shopReview.get().isDeleted()).isTrue(); - } - ); - }); + Optional shopReview = shopReviewRepository.findById(1); + assertSoftly( + softly -> { + softly.assertThat(shopReview.get().isDeleted()).isTrue(); + } + ); } @Test - void 신고가_반려된_리뷰는_is_reported_가_false_이다() { - ShopReviewReport shopReviewReport = shopReviewReportFixture.리뷰_신고(준호_학생, 익명_학생_리뷰, DISMISSED); - var response = RestAssured - .given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + token_준호) - .when() - .queryParam("limit", 10) - .queryParam("page", 1) - .pathParam("shopId", 신전_떡볶이.getId()) - .get("/shops/{shopId}/reviews") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + void 신고가_반려된_리뷰는_is_reported_가_false_이다() throws Exception { + mockMvc.perform(get("/shops/{shopId}/reviews", 신전_떡볶이.getId()) + .header("Authorization", "Bearer " + token_준호) + .queryParam("limit", "10") + .queryParam("page", "1") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + """) + ) + .andExpect(status().isOk()); - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" - { - "total_count": 2, - "current_count": 2, - "total_page": 1, - "current_page": 1, - "statistics": { - "average_rating": 4.0, - "ratings": { - "1": 0, - "2": 0, - "3": 0, - "4": 2, - "5": 0 - } - }, - "reviews": [ - { - "review_id": %d, - "rating": %d, - "nick_name": "%s", - "content": "%s", - "image_urls": [ - "%s" - ], - "menu_names": [ - "%s" - ], - "is_mine": true, - "is_modified": false, - "is_reported": false, - "created_at": "2024-01-15" - },{ - "review_id": %d, - "rating": %d, - "nick_name": "%s", - "content": "%s", - "image_urls": [ - "%s" - ], - "menu_names": [ - "%s" - ], - "is_mine": false, - "is_modified": false, - "is_reported": false, - "created_at": "2024-01-15" - } - ] - } - """, - 준호_학생_리뷰.getId(), - 준호_학생_리뷰.getRating(), - 준호_학생_리뷰.getReviewer().getUser().getNickname(), - 준호_학생_리뷰.getContent(), - 준호_학생_리뷰.getImages().get(0).getImageUrls(), - 준호_학생_리뷰.getMenus().get(0).getMenuName(), - 익명_학생_리뷰.getId(), - 익명_학생_리뷰.getRating(), - 익명_학생_리뷰.getReviewer().getAnonymousNickname(), - 익명_학생_리뷰.getContent(), - 익명_학생_리뷰.getImages().get(0).getImageUrls(), - 익명_학생_리뷰.getMenus().get(0).getMenuName()) - ); } @Test - void 단일_리뷰를_조회할_수_있다() { - var response = RestAssured - .given() - .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + token_준호) - .when() - .pathParam("shopId", 신전_떡볶이.getId()) - .pathParam("reviewId", 준호_학생_리뷰.getId()) - .get("/shops/{shopId}/reviews/{reviewId}") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + void 단일_리뷰를_조회할_수_있다() throws Exception { + mockMvc.perform(get("/shops/{shopId}/reviews/{reviewId}", 신전_떡볶이.getId(), 준호_학생_리뷰.getId()) + .header("Authorization", "Bearer " + token_준호) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" { "review_id": %d, "rating": %d, @@ -737,27 +570,20 @@ void deleteMyReview() { 준호_학생_리뷰.getContent(), 준호_학생_리뷰.getImages().get(0).getImageUrls(), 준호_학생_리뷰.getMenus().get(0).getMenuName()) - ); + )); } @Test - void 최신순으로_정렬하여_리뷰를_조회한다() { + void 최신순으로_정렬하여_리뷰를_조회한다() throws Exception { ShopReview 최신_리뷰_2024_08_07 = shopReviewFixture.최신_리뷰_2024_08_07(준호_학생, 신전_떡볶이); - var response = RestAssured - .given() - .contentType(ContentType.JSON) - .when() - .queryParam("limit", 10) - .queryParam("page", 1) - .queryParam("sorter", "LATEST") - .pathParam("shopId", 신전_떡볶이.getId()) - .get("/shops/{shopId}/reviews") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + mockMvc.perform(get("/shops/{shopId}/reviews", 신전_떡볶이.getId()) + .queryParam("limit", "10") + .queryParam("page", "1") + .queryParam("sorter", "LATEST") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" { "total_count": 3, "current_count": 3, @@ -843,27 +669,22 @@ void deleteMyReview() { 익명_학생_리뷰.getContent(), 익명_학생_리뷰.getImages().get(0).getImageUrls(), 익명_학생_리뷰.getMenus().get(0).getMenuName()) - ); + )); } @Test - void 오래된_순으로_정렬하여_리뷰를_조회한다() { + void 오래된_순으로_정렬하여_리뷰를_조회한다() throws Exception { ShopReview 최신_리뷰_2024_08_07 = shopReviewFixture.최신_리뷰_2024_08_07(준호_학생, 신전_떡볶이); - var response = RestAssured - .given() - .contentType(ContentType.JSON) - .when() - .queryParam("limit", 10) - .queryParam("page", 1) - .queryParam("sorter", "OLDEST") - .pathParam("shopId", 신전_떡볶이.getId()) - .get("/shops/{shopId}/reviews") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + mockMvc.perform(get("/shops/{shopId}/reviews", 신전_떡볶이.getId()) + .queryParam("limit", "10") + .queryParam("page", "1") + .queryParam("sorter", "OLDEST") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + """) + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" { "total_count": 3, "current_count": 3, @@ -949,27 +770,20 @@ void deleteMyReview() { 최신_리뷰_2024_08_07.getContent(), 최신_리뷰_2024_08_07.getImages().get(0).getImageUrls(), 최신_리뷰_2024_08_07.getMenus().get(0).getMenuName()) - ); + )); } @Test - void 별점이_높은_순으로_정렬하여_리뷰를_조회한다() { + void 별점이_높은_순으로_정렬하여_리뷰를_조회한다() throws Exception { ShopReview 리뷰_5점 = shopReviewFixture.리뷰_5점(준호_학생, 신전_떡볶이); - var response = RestAssured - .given() - .contentType(ContentType.JSON) - .when() - .queryParam("limit", 10) - .queryParam("page", 1) - .queryParam("sorter", "HIGHEST_RATING") - .pathParam("shopId", 신전_떡볶이.getId()) - .get("/shops/{shopId}/reviews") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + mockMvc.perform(get("/shops/{shopId}/reviews", 신전_떡볶이.getId()) + .queryParam("limit", "10") + .queryParam("page", "1") + .queryParam("sorter", "HIGHEST_RATING") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" { "total_count": 3, "current_count": 3, @@ -1055,27 +869,20 @@ void deleteMyReview() { 익명_학생_리뷰.getContent(), 익명_학생_리뷰.getImages().get(0).getImageUrls(), 익명_학생_리뷰.getMenus().get(0).getMenuName()) - ); + )); } @Test - void 별점이_낮은_순으로_정렬하여_리뷰를_조회한다() { + void 별점이_낮은_순으로_정렬하여_리뷰를_조회한다() throws Exception { ShopReview 리뷰_5점 = shopReviewFixture.리뷰_5점(준호_학생, 신전_떡볶이); - var response = RestAssured - .given() - .contentType(ContentType.JSON) - .when() - .queryParam("limit", 10) - .queryParam("page", 1) - .queryParam("sorter", "LOWEST_RATING") - .pathParam("shopId", 신전_떡볶이.getId()) - .get("/shops/{shopId}/reviews") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + mockMvc.perform(get("/shops/{shopId}/reviews", 신전_떡볶이.getId()) + .queryParam("limit", "10") + .queryParam("page", "1") + .queryParam("sorter", "LOWEST_RATING") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" { "total_count": 3, "current_count": 3, @@ -1161,26 +968,19 @@ void deleteMyReview() { 리뷰_5점.getContent(), 리뷰_5점.getImages().get(0).getImageUrls(), 리뷰_5점.getMenus().get(0).getMenuName()) - ); + )); } @Test - void 자신의_리뷰를_최신순으로_조회한다() { + void 자신의_리뷰를_최신순으로_조회한다() throws Exception { ShopReview 최신_리뷰_2024_08_07 = shopReviewFixture.최신_리뷰_2024_08_07(준호_학생, 신전_떡볶이); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token_준호) - .contentType(ContentType.JSON) - .when() - .pathParam("shopId", 신전_떡볶이.getId()) - .queryParam("sorter", "LATEST") - .get("/shops/{shopId}/reviews/me") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + mockMvc.perform(get("/shops/{shopId}/reviews/me", 신전_떡볶이.getId()) + .header("Authorization", "Bearer " + token_준호) + .queryParam("sorter", "LATEST") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" { "count": 2, "reviews": [ @@ -1229,26 +1029,19 @@ void deleteMyReview() { 준호_학생_리뷰.getContent(), 준호_학생_리뷰.getImages().get(0).getImageUrls(), 준호_학생_리뷰.getMenus().get(0).getMenuName()) - ); + )); } @Test - void 자신의_리뷰를_오래된_순으로_조회한다() { + void 자신의_리뷰를_오래된_순으로_조회한다() throws Exception { ShopReview 최신_리뷰_2024_08_07 = shopReviewFixture.최신_리뷰_2024_08_07(준호_학생, 신전_떡볶이); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token_준호) - .contentType(ContentType.JSON) - .when() - .pathParam("shopId", 신전_떡볶이.getId()) - .queryParam("sorter", "OLDEST") - .get("/shops/{shopId}/reviews/me") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + mockMvc.perform(get("/shops/{shopId}/reviews/me", 신전_떡볶이.getId()) + .header("Authorization", "Bearer " + token_준호) + .queryParam("sorter", "OLDEST") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" { "count": 2, "reviews": [ @@ -1297,26 +1090,19 @@ void deleteMyReview() { 최신_리뷰_2024_08_07.getContent(), 최신_리뷰_2024_08_07.getImages().get(0).getImageUrls(), 최신_리뷰_2024_08_07.getMenus().get(0).getMenuName()) - ); + )); } @Test - void 자신의_리뷰를_별점이_높은_순으로_조회한다() { + void 자신의_리뷰를_별점이_높은_순으로_조회한다() throws Exception { ShopReview 리뷰_5점 = shopReviewFixture.리뷰_5점(준호_학생, 신전_떡볶이); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token_준호) - .contentType(ContentType.JSON) - .when() - .pathParam("shopId", 신전_떡볶이.getId()) - .queryParam("sorter", "HIGHEST_RATING") - .get("/shops/{shopId}/reviews/me") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + mockMvc.perform(get("/shops/{shopId}/reviews/me", 신전_떡볶이.getId()) + .header("Authorization", "Bearer " + token_준호) + .queryParam("sorter", "HIGHEST_RATING") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" { "count": 2, "reviews": [ @@ -1365,26 +1151,19 @@ void deleteMyReview() { 준호_학생_리뷰.getContent(), 준호_학생_리뷰.getImages().get(0).getImageUrls(), 준호_학생_리뷰.getMenus().get(0).getMenuName()) - ); + )); } @Test - void 자신의_리뷰를_별점이_낮은_순으로_조회한다() { + void 자신의_리뷰를_별점이_낮은_순으로_조회한다() throws Exception { ShopReview 리뷰_5점 = shopReviewFixture.리뷰_5점(준호_학생, 신전_떡볶이); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token_준호) - .contentType(ContentType.JSON) - .when() - .pathParam("shopId", 신전_떡볶이.getId()) - .queryParam("sorter", "LOWEST_RATING") - .get("/shops/{shopId}/reviews/me") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + mockMvc.perform(get("/shops/{shopId}/reviews/me", 신전_떡볶이.getId()) + .header("Authorization", "Bearer " + token_준호) + .queryParam("sorter", "LOWEST_RATING") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" { "count": 2, "reviews": [ @@ -1433,7 +1212,7 @@ void deleteMyReview() { 리뷰_5점.getContent(), 리뷰_5점.getImages().get(0).getImageUrls(), 리뷰_5점.getMenus().get(0).getMenuName()) - ); + )); } } diff --git a/src/test/java/in/koreatech/koin/acceptance/TimetableApiTest.java b/src/test/java/in/koreatech/koin/acceptance/TimetableApiTest.java index 0b152c932..105a5c35f 100644 --- a/src/test/java/in/koreatech/koin/acceptance/TimetableApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/TimetableApiTest.java @@ -1,12 +1,15 @@ package in.koreatech.koin.acceptance; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -import io.restassured.response.Response; -import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.transaction.annotation.Transactional; import in.koreatech.koin.AcceptanceTest; import in.koreatech.koin.domain.timetable.model.Lecture; @@ -17,17 +20,10 @@ import in.koreatech.koin.fixture.SemesterFixture; import in.koreatech.koin.fixture.TimeTableV2Fixture; import in.koreatech.koin.fixture.UserFixture; -import in.koreatech.koin.support.JsonAssertions; -import io.restassured.RestAssured; -import io.restassured.http.ContentType; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; @SuppressWarnings("NonAsciiCharacters") +@Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class TimetableApiTest extends AcceptanceTest { @Autowired @@ -45,26 +41,26 @@ class TimetableApiTest extends AcceptanceTest { @Autowired private SemesterFixture semesterFixture; + @BeforeAll + void setup() { + clear(); + } + @Test - @DisplayName("특정 학기 강의를 조회한다") - void getSemesterLecture() { + void 특정_학기_강의를_조회한다() throws Exception { semesterFixture.semester("20192"); semesterFixture.semester("20201"); String semester = "20201"; lectureFixture.HRD_개론(semester); lectureFixture.건축구조의_이해_및_실습("20192"); - var response = RestAssured - .given() - .when() - .param("semester_date", semester) - .get("/lectures") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/lectures") + .param("semester_date", semester) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" [ { "id" : 1, @@ -84,29 +80,24 @@ void getSemesterLecture() { ] } ] - """); + """)); } @Test - @DisplayName("특정 학기 강의들을 조회한다") - void getSemesterLectures() { + void 특정_학기_강의들을_조회한다() throws Exception { semesterFixture.semester("20201"); String semester = "20201"; lectureFixture.HRD_개론(semester); lectureFixture.건축구조의_이해_및_실습(semester); lectureFixture.재료역학(semester); - var response = RestAssured - .given() - .when() - .param("semester_date", semester) - .get("/lectures") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/lectures") + .param("semester_date", semester) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" [ { "id" : 1, @@ -160,50 +151,41 @@ void getSemesterLectures() { ] } ] - """); + """)); } @Test - @DisplayName("존재하지 않는 학기를 조회하면 404") - void isNotSemester() { + void 존재하지_않는_학기를_조회하면_404() throws Exception { String semester = "20201"; lectureFixture.HRD_개론(semester); lectureFixture.건축구조의_이해_및_실습(semester); - RestAssured - .given() - .when() - .param("semester_date", "20193") - .get("/lectures") - .then() - .statusCode(HttpStatus.NOT_FOUND.value()) - .extract(); + mockMvc.perform( + get("/lectures") + .param("semester_date", "20193") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isNotFound()); } @Test - @DisplayName("계절학기를 조회하면 빈 리스트로 반환한다.") - void getSeasonLecture() { + void 계절학기를_조회하면_빈_리스트로_반환한다() throws Exception { semesterFixture.semester("20241"); semesterFixture.semester("20242"); semesterFixture.semester("2024-여름"); semesterFixture.semester("2024-겨울"); - var Response = RestAssured - .given() - .when() - .param("semester_date", "2024-여름") - .get("/lectures") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(Response.asPrettyString()) - .isEqualTo("[]"); + mockMvc.perform( + get("/lectures") + .param("semester_date", "2024-여름") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json("[]")); } @Test - @DisplayName("모든 학기를 조회한다.") - void findAllSemesters() { + void 모든_학기를_조회한다() throws Exception { semesterFixture.semester("20241"); semesterFixture.semester("20242"); semesterFixture.semester("2024-여름"); @@ -212,16 +194,12 @@ void findAllSemesters() { semesterFixture.semester("2023-여름"); semesterFixture.semester("2023-겨울"); - var response = RestAssured - .given() - .when() - .get("/semesters") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/semesters") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" [ { "id": 2, @@ -252,13 +230,11 @@ void findAllSemesters() { "semester": "20231" } ] - """); + """)); } @Test - @DisplayName("시간표를 조회한다.") - void getTimeTables() { - // given + void 시간표를_조회한다() throws Exception { User user = userFixture.준호_학생().getUser(); String token = userFixture.getToken(user); Semester semester = semesterFixture.semester("20192"); @@ -268,19 +244,14 @@ void getTimeTables() { timetableV2Fixture.시간표6(user, semester, 건축구조의_이해_및_실습, HRD_개론); - // when & then - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .param("semester", semester.getSemester()) - .get("/timetables") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + mockMvc.perform( + get("/timetables") + .header("Authorization", "Bearer " + token) + .param("semester", semester.getSemester()) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "semester": "20192", "timetable": [ @@ -318,31 +289,23 @@ void getTimeTables() { "grades": 6, "total_grades": 6 } - """ - )); + """)); } @Test - @DisplayName("시간표를 조회한다. - 시간표 프레임 없으면 생성") - void getTimeTablesAfterCreate() { - // given + void 시간표를_조회한다_시간표_프레임_없으면_생성() throws Exception { User user = userFixture.준호_학생().getUser(); String token = userFixture.getToken(user); Semester semester = semesterFixture.semester("20192"); - // when & then - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .param("semester", semester.getSemester()) - .get("/timetables") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + mockMvc.perform( + get("/timetables") + .header("Authorization", "Bearer " + token) + .param("semester", semester.getSemester()) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "semester": "20192", "timetable": [ @@ -350,13 +313,11 @@ void getTimeTablesAfterCreate() { "grades": 0, "total_grades": 0 } - """ - )); + """)); } @Test - @DisplayName("학생이 가진 시간표의 학기를 조회한다.") - void getStudentCheckSemester() { + void 학생이_가진_시간표의_학기를_조회한다() throws Exception { User user = userFixture.준호_학생().getUser(); String token = userFixture.getToken(user); Semester semester1 = semesterFixture.semester("20192"); @@ -366,18 +327,13 @@ void getStudentCheckSemester() { timetableV2Fixture.시간표6(user, semester1, HRD_개론, null); timetableV2Fixture.시간표6(user, semester2, 건축구조의_이해_및_실습, null); - - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .get("/semesters/check") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/semesters/check") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "user_id": 1, "semesters": [ @@ -385,13 +341,11 @@ void getStudentCheckSemester() { "20192" ] } - """ - ); + """)); } @Test - @DisplayName("시간표를 생성한다.") - void createTimeTables() { + void 시간표를_생성한다() throws Exception { User user = userFixture.준호_학생().getUser(); String token = userFixture.getToken(user); Semester semester = semesterFixture.semester("20192"); @@ -399,99 +353,92 @@ void createTimeTables() { lectureFixture.건축구조의_이해_및_실습(semester.getSemester()); lectureFixture.HRD_개론(semester.getSemester()); - timetableV2Fixture.시간표1(user, semester); + timetableV2Fixture.시간표1(user, semester); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType(ContentType.JSON) - .body(""" - { - "timetable": [ - { - "regular_number": "25", - "code": "ARB244", - "design_score": "0", - "class_time": [200, 201, 202, 203, 204, 205, 206, 207], - "class_place": null, - "memo": null, - "grades": "3", - "class_title": "건축구조의 이해 및 실습", - "lecture_class": "01", - "target": "디자 1 건축", - "professor": "황현식", - "department": "디자인ㆍ건축공학부" - }, - { - "regular_number": "22", - "code": "BSM590", - "design_score": "0", - "class_time": [12, 13, 14, 15, 210, 211, 212, 213], - "class_place": null, - "memo": null, - "grades": "3", - "class_title": "컴퓨팅사고", - "lecture_class": "06", - "target": "기공1", - "professor": "박한수,최준호", - "department": "기계공학부" - } - ], - "semester": "20192" - } - """) - .when() - .post("/timetables") - .then() - .statusCode(HttpStatus.OK.value()) - .extract() - .response(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" - { - "semester": "20192", - "timetable": [ - { - "id": 1, - "regular_number": "25", - "code": "ARB244", - "design_score": "0", - "class_time": [200, 201, 202, 203, 204, 205, 206, 207], - "class_place": null, - "memo": null, - "grades": "3", - "class_title": "건축구조의 이해 및 실습", - "lecture_class": "01", - "target": "디자 1 건축", - "professor": "황현식", - "department": "디자인ㆍ건축공학부" - }, + mockMvc.perform( + post("/timetables") + .header("Authorization", "Bearer " + token) + .content(""" + { + "timetable": [ + { + "regular_number": "25", + "code": "ARB244", + "design_score": "0", + "class_time": [200, 201, 202, 203, 204, 205, 206, 207], + "class_place": null, + "memo": null, + "grades": "3", + "class_title": "건축구조의 이해 및 실습", + "lecture_class": "01", + "target": "디자 1 건축", + "professor": "황현식", + "department": "디자인ㆍ건축공학부" + }, + { + "regular_number": "22", + "code": "BSM590", + "design_score": "0", + "class_time": [12, 13, 14, 15, 210, 211, 212, 213], + "class_place": null, + "memo": null, + "grades": "3", + "class_title": "컴퓨팅사고", + "lecture_class": "06", + "target": "기공1", + "professor": "박한수,최준호", + "department": "기계공학부" + } + ], + "semester": "20192" + } + """) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { - "id": 2, - "regular_number": "22", - "code": "BSM590", - "design_score": "0", - "class_time": [12, 13, 14, 15, 210, 211, 212, 213], - "class_place": null, - "memo": null, - "grades": "3", - "class_title": "컴퓨팅사고", - "lecture_class": "06", - "target": "기공1", - "professor": "박한수,최준호", - "department": "기계공학부" + "semester": "20192", + "timetable": [ + { + "id": 1, + "regular_number": "25", + "code": "ARB244", + "design_score": "0", + "class_time": [200, 201, 202, 203, 204, 205, 206, 207], + "class_place": null, + "memo": null, + "grades": "3", + "class_title": "건축구조의 이해 및 실습", + "lecture_class": "01", + "target": "디자 1 건축", + "professor": "황현식", + "department": "디자인ㆍ건축공학부" + }, + { + "id": 2, + "regular_number": "22", + "code": "BSM590", + "design_score": "0", + "class_time": [12, 13, 14, 15, 210, 211, 212, 213], + "class_place": null, + "memo": null, + "grades": "3", + "class_title": "컴퓨팅사고", + "lecture_class": "06", + "target": "기공1", + "professor": "박한수,최준호", + "department": "기계공학부" + } + ], + "grades": 6, + "total_grades": 6 } - ], - "grades": 6, - "total_grades": 6 - } - """); + """)); } @Test - @DisplayName("시간표를 단일 생성한다. - 전체 반환") - void createTimeTablesReturnAll() { + void 시간표를_단일_생성한다_전체_반환() throws Exception { User user = userFixture.준호_학생().getUser(); String token = userFixture.getToken(user); Semester semester = semesterFixture.semester("20192"); @@ -501,115 +448,104 @@ void createTimeTablesReturnAll() { timetableV2Fixture.시간표1(user, semester); - var response1 = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType(ContentType.JSON) - .body(""" - { - "timetable": [ - { - "regular_number": "25", - "code": "ARB244", - "design_score": "0", - "class_time": [200, 201, 202, 203, 204, 205, 206, 207], - "class_place": null, - "memo": null, - "grades": "3", - "class_title": "건축구조의 이해 및 실습", - "lecture_class": "01", - "target": "디자 1 건축", - "professor": "황현식", - "department": "디자인ㆍ건축공학부" - } - ], - "semester": "20192" - } - """) - .when() - .post("/timetables") - .then() - .statusCode(HttpStatus.OK.value()) - .extract() - .response(); - - var response2 = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType(ContentType.JSON) - .body(""" - { - "timetable": [ - { - "regular_number": "22", - "code": "BSM590", - "design_score": "0", - "class_time": [12, 13, 14, 15, 210, 211, 212, 213], - "class_place": null, - "memo": null, - "grades": "3", - "class_title": "컴퓨팅사고", - "lecture_class": "06", - "target": "기공1", - "professor": "박한수,최준호", - "department": "기계공학부" - } - ], - "semester": "20192" - } - """) - .when() - .post("/timetables") - .then() - .statusCode(HttpStatus.OK.value()) - .extract() - .response(); - - JsonAssertions.assertThat(response2.asPrettyString()) - .isEqualTo(""" - { - "semester": "20192", - "timetable": [ - { - "id": 1, - "regular_number": "25", - "code": "ARB244", - "design_score": "0", - "class_time": [200, 201, 202, 203, 204, 205, 206, 207], - "class_place": null, - "memo": null, - "grades": "3", - "class_title": "건축구조의 이해 및 실습", - "lecture_class": "01", - "target": "디자 1 건축", - "professor": "황현식", - "department": "디자인ㆍ건축공학부" - }, + mockMvc.perform( + post("/timetables") + .header("Authorization", "Bearer " + token) + .content(""" + { + "timetable": [ + { + "regular_number": "25", + "code": "ARB244", + "design_score": "0", + "class_time": [200, 201, 202, 203, 204, 205, 206, 207], + "class_place": null, + "memo": null, + "grades": "3", + "class_title": "건축구조의 이해 및 실습", + "lecture_class": "01", + "target": "디자 1 건축", + "professor": "황현식", + "department": "디자인ㆍ건축공학부" + } + ], + "semester": "20192" + } + """) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()); + + mockMvc.perform( + post("/timetables") + .header("Authorization", "Bearer " + token) + .content(""" + { + "timetable": [ + { + "regular_number": "22", + "code": "BSM590", + "design_score": "0", + "class_time": [12, 13, 14, 15, 210, 211, 212, 213], + "class_place": null, + "memo": null, + "grades": "3", + "class_title": "컴퓨팅사고", + "lecture_class": "06", + "target": "기공1", + "professor": "박한수,최준호", + "department": "기계공학부" + } + ], + "semester": "20192" + } + """) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { - "id": 2, - "regular_number": "22", - "code": "BSM590", - "design_score": "0", - "class_time": [12, 13, 14, 15, 210, 211, 212, 213], - "class_place": null, - "memo": null, - "grades": "3", - "class_title": "컴퓨팅사고", - "lecture_class": "06", - "target": "기공1", - "professor": "박한수,최준호", - "department": "기계공학부" + "semester": "20192", + "timetable": [ + { + "id": 1, + "regular_number": "25", + "code": "ARB244", + "design_score": "0", + "class_time": [200, 201, 202, 203, 204, 205, 206, 207], + "class_place": null, + "memo": null, + "grades": "3", + "class_title": "건축구조의 이해 및 실습", + "lecture_class": "01", + "target": "디자 1 건축", + "professor": "황현식", + "department": "디자인ㆍ건축공학부" + }, + { + "id": 2, + "regular_number": "22", + "code": "BSM590", + "design_score": "0", + "class_time": [12, 13, 14, 15, 210, 211, 212, 213], + "class_place": null, + "memo": null, + "grades": "3", + "class_title": "컴퓨팅사고", + "lecture_class": "06", + "target": "기공1", + "professor": "박한수,최준호", + "department": "기계공학부" + } + ], + "grades": 6, + "total_grades": 6 } - ], - "grades": 6, - "total_grades": 6 - } - """); + """)); } @Test - @DisplayName("시간표를 삭제한다.") - void deleteTimetable() { + void 시간표를_삭제한다() throws Exception { User user = userFixture.준호_학생().getUser(); String token = userFixture.getToken(user); Semester semester = semesterFixture.semester("20192"); @@ -619,21 +555,20 @@ void deleteTimetable() { timetableV2Fixture.시간표6(user, semester, 건축구조의_이해_및_실습, HRD_개론); - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .param("id", 2) - .delete("/timetable") - .then() - .statusCode(HttpStatus.OK.value()); + mockMvc.perform( + delete("/timetable") + .header("Authorization", "Bearer " + token) + .param("id", "2") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()); assertThat(timetableRepository.findById(2)).isNotPresent(); } - @Test - @DisplayName("시간표 삭제 동시성 예외 적절하게 처리하는지 테스트한다.") - void deleteTimetableConcurrency() throws InterruptedException { +/* @Test + @Transactional(propagation = Propagation.NOT_SUPPORTED) + void 시간표_삭제_동시성_예외_적절하게_처리하는지_테스트한다() throws InterruptedException { User user = userFixture.준호_학생().getUser(); String token = userFixture.getToken(user); Semester semester = semesterFixture.semester("20192"); @@ -670,5 +605,6 @@ void deleteTimetableConcurrency() throws InterruptedException { assertThat(timetableRepository.findById(2)).isNotPresent(); executor.shutdown(); - } + + }*/ } diff --git a/src/test/java/in/koreatech/koin/acceptance/TimetableV2ApiTest.java b/src/test/java/in/koreatech/koin/acceptance/TimetableV2ApiTest.java index ab36e67ff..f1ab28b8a 100644 --- a/src/test/java/in/koreatech/koin/acceptance/TimetableV2ApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/TimetableV2ApiTest.java @@ -1,15 +1,15 @@ package in.koreatech.koin.acceptance; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.transaction.annotation.Transactional; import in.koreatech.koin.AcceptanceTest; import in.koreatech.koin.domain.timetable.model.Lecture; @@ -22,11 +22,10 @@ import in.koreatech.koin.fixture.SemesterFixture; import in.koreatech.koin.fixture.TimeTableV2Fixture; import in.koreatech.koin.fixture.UserFixture; -import in.koreatech.koin.support.JsonAssertions; -import io.restassured.RestAssured; -import io.restassured.http.ContentType; @SuppressWarnings("NonAsciiCharacters") +@Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) public class TimetableV2ApiTest extends AcceptanceTest { @Autowired @@ -47,78 +46,70 @@ public class TimetableV2ApiTest extends AcceptanceTest { @Autowired private TimetableLectureRepositoryV2 timetableLectureRepositoryV2; + @BeforeAll + void setup() { + clear(); + } + @Test - @DisplayName("특정 시간표 frame을 생성한다") - void createTimeTablesFrame() { + void 특정_시간표_frame을_생성한다() throws Exception { User user = userFixture.준호_학생().getUser(); String token = userFixture.getToken(user); Semester semester = semesterFixture.semester("20192"); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType(ContentType.JSON) - .body(String.format(""" - { - "semester": "%s" - } - """, semester.getSemester() - )) - .when() - .post("/v2/timetables/frame") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + post("/v2/timetables/frame") + .header("Authorization", "Bearer " + token) + .content(String.format(""" + { + "semester": "%s" + } + """, semester.getSemester() + )) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "id": 1, "timetable_name": "시간표1", "is_main": true } - """); + """)); } @Test - @DisplayName("특정 시간표 frame을 수정한다") - void updateTimetableFrame() { + void 특정_시간표_frame을_수정한다() throws Exception { User user = userFixture.준호_학생().getUser(); String token = userFixture.getToken(user); Semester semester = semesterFixture.semester("20192"); TimetableFrame frame = timetableV2Fixture.시간표1(user, semester); Integer frameId = frame.getId(); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType(ContentType.JSON) - .body(String.format(""" - { - "name": "새로운 이름", - "is_main": true - } - """ - )) - .when() - .put("/v2/timetables/frame/{id}", frameId) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + put("/v2/timetables/frame/{id}", frameId) + .header("Authorization", "Bearer " + token) + .content(String.format(""" + { + "name": "새로운 이름", + "is_main": true + } + """ + )) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "id": 1, "name": "새로운 이름", "is_main": true } - """); + """)); } @Test - @DisplayName("모든 시간표 frame을 조회한다") - void getAllTimeTablesFrame() { + void 모든_시간표_frame을_조회한다() throws Exception { User user = userFixture.준호_학생().getUser(); String token = userFixture.getToken(user); Semester semester = semesterFixture.semester("20192"); @@ -126,18 +117,14 @@ void getAllTimeTablesFrame() { timetableV2Fixture.시간표1(user, semester); timetableV2Fixture.시간표2(user, semester); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .param("semester", semester.getSemester()) - .get("/v2/timetables/frames") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/v2/timetables/frames") + .header("Authorization", "Bearer " + token) + .param("semester", semester.getSemester()) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" [ { "id": 1, @@ -150,12 +137,11 @@ void getAllTimeTablesFrame() { "is_main": false } ] - """); + """)); } @Test - @DisplayName("강의를 담고 있는 특정 시간표 frame을 삭제한다") - void deleteTimeTablesFrame() { + void 강의를_담고_있는_특정_시간표_frame을_삭제한다() throws Exception { User user = userFixture.준호_학생().getUser(); String token = userFixture.getToken(user); Semester semester = semesterFixture.semester("20192"); @@ -163,22 +149,20 @@ void deleteTimeTablesFrame() { TimetableFrame frame1 = timetableV2Fixture.시간표5(user, semester, lecture); - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .param("id", frame1.getId()) - .delete("/v2/timetables/frame") - .then() - .statusCode(HttpStatus.NO_CONTENT.value()); + mockMvc.perform( + delete("/v2/timetables/frame") + .header("Authorization", "Bearer " + token) + .param("id", String.valueOf(frame1.getId())) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isNoContent()); assertThat(timetableFrameRepositoryV2.findById(frame1.getId())).isNotPresent(); assertThat(timetableLectureRepositoryV2.findById(frame1.getTimetableLectures().get(1).getId())).isNotPresent(); } @Test - @DisplayName("isMain인 frame을 삭제한다 - 다른 frame이 main으로 됨") - void deleteMainTimeTablesFrame() { + void isMain인_frame을_삭제한다_다른_frame이_main으로_됨() throws Exception { User user = userFixture.준호_학생().getUser(); String token = userFixture.getToken(user); Semester semester = semesterFixture.semester("20192"); @@ -186,15 +170,13 @@ void deleteMainTimeTablesFrame() { TimetableFrame frame1 = timetableV2Fixture.시간표1(user, semester); TimetableFrame frame2 = timetableV2Fixture.시간표2(user, semester); - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .param("id", frame1.getId()) - .delete("/v2/timetables/frame") - .then() - .statusCode(HttpStatus.NO_CONTENT.value()) - .extract(); + mockMvc.perform( + delete("/v2/timetables/frame") + .header("Authorization", "Bearer " + token) + .param("id", String.valueOf(frame1.getId())) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isNoContent()); assertThat(timetableFrameRepositoryV2.findById(frame1.getId())).isNotPresent(); @@ -203,8 +185,7 @@ void deleteMainTimeTablesFrame() { } @Test - @DisplayName("특정 시간표 frame을 삭제한다 - 본인 삭제가 아니면 403 반환") - void deleteTimeTablesFrameNoAuth() { + void 특정_시간표_frame을_삭제한다_본인_삭제가_아니면_403_반환() throws Exception { User user1 = userFixture.준호_학생().getUser(); User user2 = userFixture.성빈_학생().getUser(); String token = userFixture.getToken(user2); @@ -212,59 +193,52 @@ void deleteTimeTablesFrameNoAuth() { TimetableFrame frame1 = timetableV2Fixture.시간표1(user1, semester); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .param("id", frame1.getId()) - .delete("/v2/timetables/frame") - .then() - .statusCode(HttpStatus.FORBIDDEN.value()); + mockMvc.perform( + delete("/v2/timetables/frame") + .header("Authorization", "Bearer " + token) + .param("id", String.valueOf(frame1.getId())) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isForbidden()); } @Test - @DisplayName("시간표를 생성한다 - TimetableLecture") - void createTimetableLecture() { + void 시간표를_생성한다_TimetableLecture() throws Exception { User user = userFixture.준호_학생().getUser(); String token = userFixture.getToken(user); Semester semester = semesterFixture.semester("20192"); timetableV2Fixture.시간표1(user, semester); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType("application/json") - .body(""" - { - "timetable_frame_id" : 1, - "timetable_lecture": [ - { - "class_title": "커스텀생성1", - "class_time" : [200, 201], - "class_place" : "한기대", - "professor" : "서정빈", - "grades": "2", - "memo" : "메모" - }, - { - "class_title": "커스텀생성2", - "class_time" : [202, 203], - "class_place" : "참빛관 편의점", - "professor" : "감사 서정빈", - "grades": "1", - "memo" : "메모" - } - ] - } - """) - .when() - .post("/v2/timetables/lecture") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + post("/v2/timetables/lecture") + .header("Authorization", "Bearer " + token) + .content(""" + { + "timetable_frame_id" : 1, + "timetable_lecture": [ + { + "class_title": "커스텀생성1", + "class_time" : [200, 201], + "class_place" : "한기대", + "professor" : "서정빈", + "grades": "2", + "memo" : "메모" + }, + { + "class_title": "커스텀생성2", + "class_time" : [202, 203], + "class_place" : "참빛관 편의점", + "professor" : "감사 서정빈", + "grades": "1", + "memo" : "메모" + } + ] + } + """) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "timetable_frame_id": 1, "timetable": [ @@ -302,55 +276,49 @@ void createTimetableLecture() { "grades": 3, "total_grades": 3 } - """); + """)); } @Test - @DisplayName("시간표를 수정한다 - TimetableLecture") - void updateTimetableLecture() { + void 시간표를_수정한다_TimetableLecture() throws Exception { User user = userFixture.준호_학생().getUser(); String token = userFixture.getToken(user); Semester semester = semesterFixture.semester("20192"); TimetableFrame frame = timetableV2Fixture.시간표3(user, semester); Integer frameId = frame.getId(); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType("application/json") - .body(""" - { - "timetable_frame_id" : 1, - "timetable_lecture": [ - { - "id": 1, - "class_title": "커스텀바꿔요1", - "class_time" : [200, 201], - "class_place" : "한기대", - "professor" : "서정빈", - "grades" : "0", - "memo" : "메모한당 히히" - }, - { - "id": 2, - "class_title": "커스텀바꿔요2", - "class_time" : [202, 203], - "class_place" : "참빛관 편의점", - "professor" : "알바 서정빈", - "grades" : "0", - "memo" : "메모한당 히히" - } - ] - } - """) - .when() - .put("/v2/timetables/lecture") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + put("/v2/timetables/lecture") + .header("Authorization", "Bearer " + token) + .content(""" + { + "timetable_frame_id" : 1, + "timetable_lecture": [ + { + "id": 1, + "class_title": "커스텀바꿔요1", + "class_time" : [200, 201], + "class_place" : "한기대", + "professor" : "서정빈", + "grades" : "0", + "memo" : "메모한당 히히" + }, + { + "id": 2, + "class_title": "커스텀바꿔요2", + "class_time" : [202, 203], + "class_place" : "참빛관 편의점", + "professor" : "알바 서정빈", + "grades" : "0", + "memo" : "메모한당 히히" + } + ] + } + """) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "timetable_frame_id": 1, "timetable": [ @@ -388,12 +356,11 @@ void updateTimetableLecture() { "grades": 0, "total_grades": 0 } - """); + """)); } @Test - @DisplayName("시간표를 조회한다 - TimetableLecture") - void getTimetableLecture() { + void 시간표를_조회한다_TimetableLecture() throws Exception { User user = userFixture.준호_학생().getUser(); String token = userFixture.getToken(user); Semester semester = semesterFixture.semester("20192"); @@ -403,19 +370,14 @@ void getTimetableLecture() { TimetableFrame frame = timetableV2Fixture.시간표6(user, semester, 건축구조의_이해_및_실습, HRD_개론); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType("application/json") - .when() - .param("timetable_frame_id", frame.getId()) - .get("/v2/timetables/lecture") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/v2/timetables/lecture") + .header("Authorization", "Bearer " + token) + .param("timetable_frame_id", String.valueOf(frame.getId())) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "timetable_frame_id": 1, "timetable": [ @@ -453,12 +415,11 @@ void getTimetableLecture() { "grades": 6, "total_grades": 6 } - """); + """)); } @Test - @DisplayName("시간표에서 특정 강의를 삭제한다") - void deleteTimetableLecture() { + void 시간표에서_특정_강의를_삭제한다() throws Exception { User user1 = userFixture.준호_학생().getUser(); String token = userFixture.getToken(user1); Semester semester = semesterFixture.semester("20192"); @@ -468,18 +429,18 @@ void deleteTimetableLecture() { Integer lectureId = lecture1.getId(); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .delete("/v2/timetables/lecture/{id}", lectureId) - .then() - .statusCode(HttpStatus.NO_CONTENT.value()); + mockMvc.perform( + delete("/v2/timetables/lecture/{id}", lectureId) + .header("Authorization", "Bearer " + token) + .param("timetable_frame_id", String.valueOf(frame.getId())) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isNoContent()); } - @Test - @DisplayName("isMain이 false인 frame과 true인 frame을 동시에 삭제한다.") - void deleteNotMainAndMainTimeTablesFrame() { + /*@Test + @Transactional(propagation = Propagation.NOT_SUPPORTED) + void isMain이_false인_frame과_true인_frame을_동시에_삭제한다() { User user = userFixture.준호_학생().getUser(); String token = userFixture.getToken(user); Semester semester = semesterFixture.semester("20192"); @@ -532,5 +493,5 @@ void deleteNotMainAndMainTimeTablesFrame() { TimetableFrame reloadedFrame2 = timetableFrameRepositoryV2.findById(frame2.getId()).orElseThrow(); assertThat(reloadedFrame2.isMain()).isTrue(); - } + }*/ } diff --git a/src/test/java/in/koreatech/koin/acceptance/TrackApiTest.java b/src/test/java/in/koreatech/koin/acceptance/TrackApiTest.java index bb724fa27..73395f629 100644 --- a/src/test/java/in/koreatech/koin/acceptance/TrackApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/TrackApiTest.java @@ -1,19 +1,24 @@ package in.koreatech.koin.acceptance; -import org.junit.jupiter.api.DisplayName; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.transaction.annotation.Transactional; import in.koreatech.koin.AcceptanceTest; import in.koreatech.koin.domain.member.model.Track; import in.koreatech.koin.fixture.MemberFixture; import in.koreatech.koin.fixture.TechStackFixture; import in.koreatech.koin.fixture.TrackFixture; -import in.koreatech.koin.support.JsonAssertions; -import io.restassured.RestAssured; @SuppressWarnings("NonAsciiCharacters") +@Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class TrackApiTest extends AcceptanceTest { @Autowired @@ -25,23 +30,23 @@ class TrackApiTest extends AcceptanceTest { @Autowired private TechStackFixture techStackFixture; + @BeforeAll + void setup() { + clear(); + } + @Test - @DisplayName("BCSDLab 트랙 정보를 조회한다") - void findTracks() { + void BCSDLab_트랙_정보를_조회한다() throws Exception { trackFixture.backend(); trackFixture.frontend(); trackFixture.ios(); - var response = RestAssured - .given() - .when() - .get("/tracks") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/tracks") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" [ { "id": 1, @@ -68,27 +73,22 @@ void findTracks() { "updated_at": "2024-01-15 12:00:00" } ] - """); + """)); } @Test - @DisplayName("BCSDLab 트랙 정보 단건 조회 - 삭제된 멤버는 조회하지 않는다.") - void findTrackWithoutDeletedMember() { + void BCSDLab_트랙_정보_단건_조회_삭제된_멤버는_조회하지_않는다() throws Exception { Track track = trackFixture.backend(); memberFixture.배진호(track); // 삭제된 멤버 memberFixture.최준호(track); techStackFixture.java(track); - var response = RestAssured - .given() - .when() - .get("/tracks/{id}", track.getId()) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/tracks/{id}", track.getId()) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "TrackName": "BackEnd", "TechStacks": [ @@ -118,26 +118,21 @@ void findTrackWithoutDeletedMember() { } ] } - """); + """)); } @Test - @DisplayName("BCSDLab 트랙 정보 단건 조회") - void findTrack() { + void BCSDLab_트랙_정보_단건_조회() throws Exception { Track track = trackFixture.backend(); memberFixture.최준호(track); techStackFixture.java(track); - var response = RestAssured - .given() - .when() - .get("/tracks/{id}", track.getId()) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/tracks/{id}", track.getId()) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "TrackName": "BackEnd", "TechStacks": [ @@ -167,24 +162,19 @@ void findTrack() { } ] } - """); + """)); } @Test - @DisplayName("BCSDLab 트랙 정보 단건 조회 - 트랙에 속한 멤버와 기술스택이 없을 때") - void findTrackWithEmptyMembersAndTechStacks() { + void BCSDLab_트랙_정보_단건_조회_트랙에_속한_멤버와_기술스택이_없을_때() throws Exception { Track track = trackFixture.frontend(); - var response = RestAssured - .given() - .when() - .get("/tracks/{id}", track.getId()) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/tracks/{id}", track.getId()) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "TrackName": "FrontEnd", "TechStacks": [ @@ -194,6 +184,6 @@ void findTrackWithEmptyMembersAndTechStacks() { ] } - """); + """)); } } diff --git a/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java b/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java index 54a78c1cd..61864152b 100644 --- a/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/UserApiTest.java @@ -6,24 +6,18 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import java.util.Optional; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import in.koreatech.koin.domain.user.model.redis.StudentTemporaryStatus; -import in.koreatech.koin.domain.user.repository.StudentRedisRepository; import org.assertj.core.api.SoftAssertions; -import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.system.CapturedOutput; -import org.springframework.boot.test.system.OutputCaptureExtension; -import org.springframework.http.HttpStatus; -import org.springframework.transaction.TransactionStatus; -import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.springframework.http.MediaType; +import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; import in.koreatech.koin.AcceptanceTest; @@ -32,15 +26,16 @@ import in.koreatech.koin.domain.user.model.Student; import in.koreatech.koin.domain.user.model.User; import in.koreatech.koin.domain.user.model.UserGender; +import in.koreatech.koin.domain.user.model.redis.StudentTemporaryStatus; +import in.koreatech.koin.domain.user.repository.StudentRedisRepository; import in.koreatech.koin.domain.user.repository.StudentRepository; import in.koreatech.koin.domain.user.repository.UserRepository; import in.koreatech.koin.fixture.UserFixture; import in.koreatech.koin.global.auth.JwtProvider; -import in.koreatech.koin.support.JsonAssertions; -import io.restassured.RestAssured; -import io.restassured.http.ContentType; @SuppressWarnings("NonAsciiCharacters") +@Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class UserApiTest extends AcceptanceTest { @Autowired @@ -61,108 +56,93 @@ class UserApiTest extends AcceptanceTest { @Autowired private UserFixture userFixture; + @BeforeAll + void setup() { + clear(); + } + @Test - @DisplayName("학생이 로그인을 진행한다(구 API(/user/login))") - void login() { + void 학생이_로그인을_진행한다_구_API_user_login() throws Exception { Student student = userFixture.성빈_학생(); String email = student.getUser().getEmail(); String password = "1234"; - var response = RestAssured - .given() - .contentType(ContentType.JSON) - .body(""" - { - "email" : "%s", - "password" : "%s" - } - """.formatted(email, password)) - .when() - .post("/user/login") - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); + mockMvc.perform( + post("/user/login") + .content(""" + { + "email" : "%s", + "password" : "%s" + } + """.formatted(email, password)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isCreated()); } @Test - @DisplayName("학생이 로그인을 진행한다(신규 API(/student/login))") - void studentLogin() { + void 학생이_로그인을_진행한다_신규_API_student_login() throws Exception { Student student = userFixture.성빈_학생(); String email = student.getUser().getEmail(); String password = "1234"; - var response = RestAssured - .given() - .contentType(ContentType.JSON) - .body(""" - { - "email" : "%s", - "password" : "%s" - } - """.formatted(email, password)) - .when() - .post("/student/login") - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); + mockMvc.perform( + post("/student/login") + .content(""" + { + "email" : "%s", + "password" : "%s" + } + """.formatted(email, password)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isCreated()); } @Test - @DisplayName("영양사가 로그인을 진행한다") - void coopLogin() { + void 영양사가_로그인을_진행한다() throws Exception { Coop coop = userFixture.준기_영양사(); String id = coop.getCoopId(); String password = "1234"; - var response = RestAssured - .given() - .contentType(ContentType.JSON) - .body(""" - { - "id" : "%s", - "password" : "%s" - } - """.formatted(id, password)) - .when() - .post("/coop/login") - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); + mockMvc.perform( + post("/coop/login") + .content(""" + { + "id" : "%s", + "password" : "%s" + } + """.formatted(id, password)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isCreated()); } @Test - @DisplayName("올바른 영양사 계정인지 확인한다") - void coopCheckMe() { + void 올바른_영양사_계정인지_확인한다() throws Exception { Coop coop = userFixture.준기_영양사(); String token = userFixture.getToken(coop.getUser()); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .get("/user/coop/me") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + mockMvc.perform( + get("/user/coop/me") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()); } @Test - @DisplayName("올바른 학생계정인지 확인한다") - void studentCheckMe() { + void 올바른_학생계정인지_확인한다() throws Exception { Student student = userFixture.준호_학생(); String token = userFixture.getToken(student.getUser()); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .get("/user/student/me") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/user/student/me") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "anonymous_nickname": "익명", "email": "juno@koreatech.ac.kr", @@ -173,70 +153,72 @@ void studentCheckMe() { "phone_number": "01012345678", "student_number": "2019136135" } - """); + """)); } @Test - @DisplayName("올바른 학생계정인지 확인한다 - 토큰 정보가 올바르지 않으면 401") - void studentCheckMeUnAuthorized() { + void 올바른_학생계정인지_확인한다_토큰_정보가_올바르지_않으면_401() throws Exception { userFixture.준호_학생(); String token = "invalidToken"; - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .get("/user/student/me") - .then() - .statusCode(HttpStatus.UNAUTHORIZED.value()) - .extract(); + mockMvc.perform( + get("/user/student/me") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isUnauthorized()); } @Test - @DisplayName("올바른 학생계정인지 확인한다 - 회원을 찾을 수 없으면 404") - void studentCheckMeNotFound() { + void 올바른_학생계정인지_확인한다_회원을_찾을_수_없으면_404() throws Exception { Student student = userFixture.준호_학생(); String token = jwtProvider.createToken(student.getUser()); transactionTemplate.executeWithoutResult(status -> studentRepository.deleteByUserId(student.getId()) ); - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .get("/user/student/me") - .then() - .statusCode(HttpStatus.NOT_FOUND.value()) - .extract(); + mockMvc.perform( + get("/user/student/me") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isNotFound()); } @Test - @DisplayName("학생이 정보를 수정한다") - void studentUpdateMe() { + void 학생이_정보를_수정한다() throws Exception { Student student = userFixture.준호_학생(); String token = userFixture.getToken(student.getUser()); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType(ContentType.JSON) - .body(""" - { - "gender" : 1, - "major" : "기계공학부", - "name" : "서정빈", - "password" : "0c4be6acaba1839d3433c1ccf04e1eec4d1fa841ee37cb019addc269e8bc1b77", - "nickname" : "duehee", - "phone_number" : "01023456789", - "student_number" : "2019136136" - } - """) - .when() - .put("/user/student/me") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + mockMvc.perform( + put("/user/student/me") + .header("Authorization", "Bearer " + token) + .content(""" + { + "gender" : 1, + "major" : "기계공학부", + "name" : "서정빈", + "password" : "0c4be6acaba1839d3433c1ccf04e1eec4d1fa841ee37cb019addc269e8bc1b77", + "nickname" : "duehee", + "phone_number" : "01023456789", + "student_number" : "2019136136" + } + """) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { + "anonymous_nickname": "익명", + "email": "juno@koreatech.ac.kr", + "gender": 1, + "major": "기계공학부", + "name": "서정빈", + "nickname": "duehee", + "phone_number": "01023456789", + "student_number": "2019136136" + } + """)); transactionTemplate.executeWithoutResult(status -> { Student result = studentRepository.getById(student.getId()); @@ -250,33 +232,17 @@ void studentUpdateMe() { } ); }); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" - { - "anonymous_nickname": "익명", - "email": "juno@koreatech.ac.kr", - "gender": 1, - "major": "기계공학부", - "name": "서정빈", - "nickname": "duehee", - "phone_number": "01023456789", - "student_number": "2019136136" - } - """); } @Test - @DisplayName("학생이 정보를 수정한다 - 학번의 형식이 맞지 않으면 400") - void studentUpdateMeNotValidStudentNumber() { + void 학생이_정보를_수정한다_학번의_형식이_맞지_않으면_400() throws Exception { Student student = userFixture.준호_학생(); String token = userFixture.getToken(student.getUser()); - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType(ContentType.JSON) - .body(""" + mockMvc.perform( + put("/user/student/me") + .header("Authorization", "Bearer " + token) + .content(""" { "gender" : 0, "major" : "메카트로닉스공학부", @@ -286,24 +252,20 @@ void studentUpdateMeNotValidStudentNumber() { "student_number" : "201913613" } """) - .when() - .put("/user/student/me") - .then() - .statusCode(HttpStatus.BAD_REQUEST.value()) - .extract(); + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isBadRequest()); } @Test - @DisplayName("학생이 정보를 수정한다 - 학부의 형식이 맞지 않으면 400") - void studentUpdateMeNotValidDepartment() { + void 학생이_정보를_수정한다_학부의_형식이_맞지_않으면_400() throws Exception { Student student = userFixture.준호_학생(); String token = userFixture.getToken(student.getUser()); - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType(ContentType.JSON) - .body(""" + mockMvc.perform( + put("/user/student/me") + .header("Authorization", "Bearer " + token) + .content(""" { "gender" : 0, "major" : "경영학과", @@ -313,24 +275,20 @@ void studentUpdateMeNotValidDepartment() { "student_number" : "2019136136" } """) - .when() - .put("/user/student/me") - .then() - .statusCode(HttpStatus.BAD_REQUEST.value()) - .extract(); + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isBadRequest()); } @Test - @DisplayName("학생이 정보를 수정한다 - 토큰이 올바르지 않다면 401") - void studentUpdateMeUnAuthorized() { + void 학생이_정보를_수정한다_토큰이_올바르지_않다면_401() throws Exception { userFixture.준호_학생(); String token = "invalidToken"; - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType(ContentType.JSON) - .body(""" + mockMvc.perform( + put("/user/student/me") + .header("Authorization", "Bearer " + token) + .content(""" { "gender" : 0, "major" : "메카트로닉스공학부", @@ -340,27 +298,23 @@ void studentUpdateMeUnAuthorized() { "student_number" : "2019136136" } """) - .when() - .put("/user/student/me") - .then() - .statusCode(HttpStatus.UNAUTHORIZED.value()) - .extract(); + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isUnauthorized()); } @Test - @DisplayName("학생이 정보를 수정한다 - 회원을 찾을 수 없다면 404") - void studentUpdateMeNotFound() { + void 학생이_정보를_수정한다_회원을_찾을_수_없다면_404() throws Exception { Student student = userFixture.준호_학생(); String token = userFixture.getToken(student.getUser()); transactionTemplate.executeWithoutResult(status -> studentRepository.deleteByUserId(student.getId()) ); - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType(ContentType.JSON) - .body(""" + mockMvc.perform( + put("/user/student/me") + .header("Authorization", "Bearer " + token) + .content(""" { "gender" : 0, "major" : "메카트로닉스공학부", @@ -370,25 +324,21 @@ void studentUpdateMeNotFound() { "student_number" : "2019136136" } """) - .when() - .put("/user/student/me") - .then() - .statusCode(HttpStatus.NOT_FOUND.value()) - .extract(); + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isNotFound()); } @Test - @DisplayName("학생이 정보를 수정한다 - 이미 있는 닉네임이라면 409") - void studentUpdateMeDuplicationNickname() { + void 학생이_정보를_수정한다_이미_있는_닉네임이라면_409() throws Exception { Student 준호 = userFixture.준호_학생(); Student 성빈 = userFixture.성빈_학생(); String token = userFixture.getToken(준호.getUser()); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType(ContentType.JSON) - .body(String.format(""" + mockMvc.perform( + put("/user/student/me") + .header("Authorization", "Bearer " + token) + .content(String.format(""" { "gender" : 0, "major" : "테스트학과", @@ -398,128 +348,101 @@ void studentUpdateMeDuplicationNickname() { "student_number" : "2019136136" } """, 성빈.getUser().getNickname())) - .when() - .put("/user/student/me") - .then() - .statusCode(HttpStatus.CONFLICT.value()) - .extract(); + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isConflict()); } @Test - @DisplayName("회원이 탈퇴한다") - void userWithdraw() { + void 회원이_탈퇴한다() throws Exception { Student student = userFixture.성빈_학생(); String token = userFixture.getToken(student.getUser()); - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .delete("/user") - .then() - .statusCode(HttpStatus.NO_CONTENT.value()) - .extract(); + mockMvc.perform( + delete("/user") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isNoContent()); assertThat(userRepository.findById(student.getId())).isNotPresent(); } @Test - @DisplayName("이메일이 중복인지 확인한다") - void emailCheckExists() { + void 이메일이_중복인지_확인한다() throws Exception { String email = "notduplicated@koreatech.ac.kr"; - RestAssured - .given() - .param("address", email) - .when() - .get("/user/check/email") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + mockMvc.perform( + get("/user/check/email") + .param("address", email) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()); assertThat(userRepository.findByEmail(email)).isNotPresent(); } @Test - @DisplayName("이메일이 중복인지 확인한다 - 파라미터에 이메일을 포함하지 않으면 400") - void emailCheckExistsNull() { - RestAssured - .when() - .get("/user/check/email") - .then() - .statusCode(HttpStatus.BAD_REQUEST.value()) - .extract(); + void 이메일이_중복인지_확인한다_파라미터에_이메일을_포함하지_않으면_400() throws Exception { + mockMvc.perform( + get("/user/check/email") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isBadRequest()); } @Test - @DisplayName("이메일이 중복인지 확인한다 - 잘못된 이메일 형식이면 400") - void emailCheckExistsWrongFormat() { + void 이메일이_중복인지_확인한다_잘못된_이메일_형식이면_400() throws Exception { String email = "wrong email format"; - RestAssured - .given() - .param("address", email) - .when() - .get("/user/check/email") - .then() - .statusCode(HttpStatus.BAD_REQUEST.value()) - .extract(); + mockMvc.perform( + get("/user/check/email") + .param("address", email) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isBadRequest()); } @Test - @DisplayName("이메일이 중복인지 확인한다 - 중복이면 422") - void emailCheckExistsAlreadyExists() { + void 이메일이_중복인지_확인한다_중복이면_422() throws Exception { User user = userFixture.성빈_학생().getUser(); - var response = RestAssured - .given() - .param("address", user.getEmail()) - .when() - .get("/user/check/email") - .then() - .statusCode(HttpStatus.CONFLICT.value()) - .extract(); - - assertThat(response.body().jsonPath().getString("message")) - .contains("존재하는 이메일입니다."); + mockMvc.perform( + get("/user/check/email") + .param("address", user.getEmail()) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.message").value("존재하는 이메일입니다.")); } @Test - @DisplayName("닉네임 중복일때 상태코드 409를 반환한다.") - void checkDuplicationOfNicknameConflict() { + void 닉네임_중복일때_상태코드_409를_반환한다() throws Exception { User user = userFixture.성빈_학생().getUser(); - var response = RestAssured - .given() - .when() - .param("nickname", user.getNickname()) - .get("/user/check/nickname") - .then() - .statusCode(HttpStatus.CONFLICT.value()) - .extract(); - - assertThat(response.body().jsonPath().getString("message")) - .contains("이미 존재하는 닉네임입니다."); + mockMvc.perform( + get("/user/check/nickname") + .param("nickname", user.getNickname()) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isConflict()) + .andExpect(jsonPath("$.message").value("이미 존재하는 닉네임입니다.")); } @Test - @DisplayName("닉네임 중복이 아닐시 상태코드 200을 반환한다.") - void checkDuplicationOfNickname() { + void 닉네임_중복이_아닐시_상태코드_200을_반환한다() throws Exception { User user = userFixture.성빈_학생().getUser(); - RestAssured - .given() - .when() - .param("nickname", "철수") - .get("/user/check/nickname") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + mockMvc.perform( + get("/user/check/nickname") + .param("nickname", "철수") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()); } @Test - @DisplayName("닉네임 제약조건 위반시 상태코드 400를 반환한다.") - void checkDuplicationOfNicknameBadRequest() { + void 닉네임_제약조건_위반시_상태코드_400를_반환한다() throws Exception { User user = User.builder() .password("1234") .nickname("주노") @@ -534,28 +457,23 @@ void checkDuplicationOfNicknameBadRequest() { userRepository.save(user); - RestAssured - .given() - .when() - .param("nickname", "철".repeat(11)) - .get("/user/check/nickname") - .then() - .statusCode(HttpStatus.BAD_REQUEST.value()) - .extract(); - - RestAssured - .given() - .when() - .param("nickname", "") - .get("/user/check/nickname") - .then() - .statusCode(HttpStatus.BAD_REQUEST.value()) - .extract(); + mockMvc.perform( + get("/user/check/nickname") + .param("nickname", "철".repeat(11)) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isBadRequest()); + + mockMvc.perform( + get("/user/check/nickname") + .param("nickname", "") + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isBadRequest()); } @Test - @DisplayName("로그인된 사용자의 권한을 조회한다.") - void getAuth() { + void 로그인된_사용자의_권한을_조회한다() throws Exception { Student student = Student.builder() .studentNumber("2019136135") .anonymousNickname("익명") @@ -579,248 +497,213 @@ void getAuth() { studentRepository.save(student); String token = jwtProvider.createToken(student.getUser()); - - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .get("/user/auth") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - User user = student.getUser(); - assertSoftly( - softly -> { - softly.assertThat(response.body().jsonPath().getString("user_type")) - .isEqualTo(user.getUserType().getValue()); - } - ); + mockMvc.perform( + get("/user/auth") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.user_type").value(user.getUserType().getValue())); } @Test - @DisplayName("학생 회원가입 후 학교 이메일요청 이벤트가 발생하고 Redis에 저장된다.") - void studentRegister() { - var response = RestAssured - .given() - .body(""" - { - "major": "컴퓨터공학부", - "email": "koko123@koreatech.ac.kr", - "name": "김철수", - "password": "cd06f8c2b0dd065faf6ef910c7f15934363df71c33740fd245590665286ed268", - "nickname": "koko", - "gender": "0", - "is_graduated": false, - "student_number": "2021136012", - "phone_number": "01000000000" - } - """) - .contentType(ContentType.JSON) - .when() - .post("/user/student/register") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - transactionTemplate.execute(new TransactionCallbackWithoutResult() { - @Override - protected void doInTransactionWithoutResult(TransactionStatus status) { - Optional student = studentRedisRepository.findById("koko123@koreatech.ac.kr"); - - assertSoftly( - softly -> { - softly.assertThat(student).isNotNull(); - softly.assertThat(student.get().getNickname()).isEqualTo("koko"); - softly.assertThat(student.get().getName()).isEqualTo("김철수"); - softly.assertThat(student.get().getPhoneNumber()).isEqualTo("01000000000"); - softly.assertThat(student.get().getEmail()).isEqualTo("koko123@koreatech.ac.kr"); - softly.assertThat(student.get().getStudentNumber()).isEqualTo("2021136012"); - softly.assertThat(student.get().getDepartment()).isEqualTo(Dept.COMPUTER_SCIENCE.getName()); - verify(studentEventListener).onStudentEmailRequest(any()); + void 학생_회원가입_후_학교_이메일요청_이벤트가_발생하고_Redis에_저장된다() throws Exception { + mockMvc.perform( + post("/user/student/register") + .content(""" + { + "major": "컴퓨터공학부", + "email": "koko123@koreatech.ac.kr", + "name": "김철수", + "password": "cd06f8c2b0dd065faf6ef910c7f15934363df71c33740fd245590665286ed268", + "nickname": "koko", + "gender": "0", + "is_graduated": false, + "student_number": "2021136012", + "phone_number": "01000000000" } - ); - } - }); - } + """) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()); - @Test - @DisplayName("이메일 요청을 확인 후 회원가입 이벤트가 발생하고 Redis에 저장된 정보가 삭제된다.") - void authenticate() { - RestAssured - .given() - .body(""" - { - "major": "컴퓨터공학부", - "email": "koko123@koreatech.ac.kr", - "name": "김철수", - "password": "cd06f8c2b0dd065faf6ef910c7f15934363df71c33740fd245590665286ed268", - "nickname": "koko", - "gender": "0", - "is_graduated": false, - "student_number": "2021136012", - "phone_number": "01000000000" + Optional student = studentRedisRepository.findById("koko123@koreatech.ac.kr"); + + assertSoftly( + softly -> { + softly.assertThat(student).isNotNull(); + softly.assertThat(student.get().getNickname()).isEqualTo("koko"); + softly.assertThat(student.get().getName()).isEqualTo("김철수"); + softly.assertThat(student.get().getPhoneNumber()).isEqualTo("01000000000"); + softly.assertThat(student.get().getEmail()).isEqualTo("koko123@koreatech.ac.kr"); + softly.assertThat(student.get().getStudentNumber()).isEqualTo("2021136012"); + softly.assertThat(student.get().getDepartment()).isEqualTo(Dept.COMPUTER_SCIENCE.getName()); + forceVerify(() -> verify(studentEventListener).onStudentEmailRequest(any())); + clear(); + setup(); } - """) - .contentType(ContentType.JSON) - .when() - .post("/user/student/register") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + ); +} + + @Test + void 이메일_요청을_확인_후_회원가입_이벤트가_발생하고_Redis에_저장된_정보가_삭제된다() throws Exception { + mockMvc.perform( + post("/user/student/register") + .content(""" + { + "major": "컴퓨터공학부", + "email": "koko123@koreatech.ac.kr", + "name": "김철수", + "password": "cd06f8c2b0dd065faf6ef910c7f15934363df71c33740fd245590665286ed268", + "nickname": "koko", + "gender": "0", + "is_graduated": false, + "student_number": "2021136012", + "phone_number": "01000000000" + } + """) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()); - Optional student = studentRedisRepository.findById("koko123@koreatech.ac.kr"); - RestAssured - .given() - .param("auth_token", student.get().getAuthToken()) - .when() - .get("/user/authenticate") - .then(); + Optional student = studentRedisRepository.findById("koko123@koreatech.ac.kr"); + mockMvc.perform( + get("/user/authenticate") + .queryParam("auth_token", student.get().getAuthToken()) + .contentType(MediaType.APPLICATION_JSON) + ) + .andReturn(); User user = userRepository.getByEmail("koko123@koreatech.ac.kr"); assertThat(studentRedisRepository.findById("koko123@koreatech.ac.kr")).isEmpty(); assertThat(user.isAuthed()).isTrue(); - verify(studentEventListener).onStudentRegister(any()); + forceVerify(() -> verify(studentEventListener).onStudentRegister(any())); + clear(); + setup(); } @Test - @DisplayName("회원 가입 필수 파라미터를 안넣을시 400에러코드를 반환한다.") - void studentRegisterBadRequest() { - RestAssured - .given() - .body(""" - { - "major": "컴퓨터공학부", - "email": null, - "name": "김철수", - "password": "cd06f8c2b0dd065faf6ef910c7f15934363df71c33740fd245590665286ed268", - "nickname": "koko", - "gender": "0", - "is_graduated": false, - "student_number": "2021136012", - "phone_number": "01000000000" - } - """) - .contentType(ContentType.JSON) - .when() - .post("/user/student/register") - .then() - .statusCode(HttpStatus.BAD_REQUEST.value()); + void 회원_가입_필수_파라미터를_안넣을시_400에러코드를_반환한다() throws Exception { + mockMvc.perform( + post("/user/student/register") + .content(""" + { + "major": "컴퓨터공학부", + "email": null, + "name": "김철수", + "password": "cd06f8c2b0dd065faf6ef910c7f15934363df71c33740fd245590665286ed268", + "nickname": "koko", + "gender": "0", + "is_graduated": false, + "student_number": "2021136012", + "phone_number": "01000000000" + } + """) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isBadRequest()); } @Test - @DisplayName("한기대 이메일이 아닐시 400에러코드를 반환한다.") - void studentRegisterInvalid() { - RestAssured - .given() - .body(""" - { - "major": "컴퓨터공학부", - "email": "koko123@gmail.com", - "name": "김철수", - "password": "cd06f8c2b0dd065faf6ef910c7f15934363df71c33740fd245590665286ed268", - "nickname": "koko", - "gender": "0", - "is_graduated": false, - "student_number": "2021136012", - "phone_number": "01000000000" - } - """) - .contentType(ContentType.JSON) - .when() - .post("/user/student/register") - .then() - .statusCode(HttpStatus.BAD_REQUEST.value()); + void 한기대_이메일이_아닐시_400에러코드를_반환한다() throws Exception { + mockMvc.perform( + post("/user/student/register") + .content(""" + { + "major": "컴퓨터공학부", + "email": "koko123@gmail.com", + "name": "김철수", + "password": "cd06f8c2b0dd065faf6ef910c7f15934363df71c33740fd245590665286ed268", + "nickname": "koko", + "gender": "0", + "is_graduated": false, + "student_number": "2021136012", + "phone_number": "01000000000" + } + """) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isBadRequest()); } @Test - @DisplayName("유효한 학번의 형식이 아닐시 400에러코드를 반환한다.") - void studentRegisterStudentNumberInvalid() { - RestAssured - .given() - .body(""" - { - "major": "컴퓨터공학부", - "email": "koko123@gmail.com", - "name": "김철수", - "password": "cd06f8c2b0dd065faf6ef910c7f15934363df71c33740fd245590665286ed268", - "nickname": "koko", - "gender": "0", - "is_graduated": false, - "student_number": "20211360123324231", - "phone_number": "01000000000" - } - """) - .contentType(ContentType.JSON) - .when() - .post("/user/student/register") - .then() - .statusCode(HttpStatus.BAD_REQUEST.value()); - - RestAssured - .given() - .body(""" - { - "major": "컴퓨터공학부", - "email": "koko123@gmail.com", - "name": "김철수", - "password": "cd06f8c2b0dd065faf6ef910c7f15934363df71c33740fd245590665286ed268", - "nickname": "koko", - "gender": "0", - "is_graduated": false, - "student_number": "19911360123", - "phone_number": "01000000000" - } - """) - .contentType(ContentType.JSON) - .when() - .post("/user/student/register") - .then() - .statusCode(HttpStatus.BAD_REQUEST.value()); + void 유효한_학번의_형식이_아닐시_400에러코드를_반환한다() throws Exception { + mockMvc.perform( + post("/user/student/register") + .content(""" + { + "major": "컴퓨터공학부", + "email": "koko123@gmail.com", + "name": "김철수", + "password": "cd06f8c2b0dd065faf6ef910c7f15934363df71c33740fd245590665286ed268", + "nickname": "koko", + "gender": "0", + "is_graduated": false, + "student_number": "20211360123324231", + "phone_number": "01000000000" + } + """) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isBadRequest()); + + mockMvc.perform( + post("/user/student/register") + .content(""" + { + "major": "컴퓨터공학부", + "email": "koko123@gmail.com", + "name": "김철수", + "password": "cd06f8c2b0dd065faf6ef910c7f15934363df71c33740fd245590665286ed268", + "nickname": "koko", + "gender": "0", + "is_graduated": false, + "student_number": "19911360123", + "phone_number": "01000000000" + } + """) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isBadRequest()); } @Test - @DisplayName("사용자가 비밀번호를 통해 자신이 맞는지 인증한다.") - void userCheckPassword() { + void 사용자가_비밀번호를_통해_자신이_맞는지_인증한다() throws Exception { Student student = userFixture.준호_학생(); String token = userFixture.getToken(student.getUser()); - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType(ContentType.JSON) - .body(""" - { - "password": "1234" - } - """) - .when() - .post("/user/check/password") - .then() - .statusCode(HttpStatus.OK.value()); + mockMvc.perform( + post("/user/check/password") + .header("Authorization", "Bearer " + token) + .content(""" + { + "password": "1234" + } + """) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()); } @Test - @DisplayName("사용자가 비밀번호를 통해 자신이 맞는지 인증한다. - 비밀번호가 다르면 400 반환") - void userCheckPasswordInvalid() { + void 사용자가_비밀번호를_통해_자신이_맞는지_인증한다_비밀번호가_다르면_400_반환() throws Exception { Student student = userFixture.준호_학생(); String token = userFixture.getToken(student.getUser()); - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType(ContentType.JSON) - .body(""" - { - "password": "1233" - } - """) - .when() - .post("/user/check/password") - .then() - .statusCode(HttpStatus.BAD_REQUEST.value()); + mockMvc.perform( + post("/user/check/password") + .header("Authorization", "Bearer " + token) + .content(""" + { + "password": "123" + } + """) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isBadRequest()); } } diff --git a/src/test/java/in/koreatech/koin/acceptance/VersionApiTest.java b/src/test/java/in/koreatech/koin/acceptance/VersionApiTest.java index aa03398a0..14664f789 100644 --- a/src/test/java/in/koreatech/koin/acceptance/VersionApiTest.java +++ b/src/test/java/in/koreatech/koin/acceptance/VersionApiTest.java @@ -1,26 +1,35 @@ package in.koreatech.koin.acceptance; -import org.junit.jupiter.api.DisplayName; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.transaction.annotation.Transactional; import in.koreatech.koin.AcceptanceTest; import in.koreatech.koin.domain.version.model.Version; import in.koreatech.koin.domain.version.model.VersionType; import in.koreatech.koin.domain.version.repository.VersionRepository; -import in.koreatech.koin.support.JsonAssertions; -import io.restassured.RestAssured; @SuppressWarnings("NonAsciiCharacters") +@Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class VersionApiTest extends AcceptanceTest { @Autowired private VersionRepository versionRepository; + @BeforeAll + void setup() { + clear(); + } + @Test - @DisplayName("버전 타입을 통해 버전 정보를 조회한다.") - void findVersionByType() { + void 버전_타입을_통해_버전_정보를_조회한다() throws Exception { Version version = versionRepository.save( Version.builder() .version("1.0.0") @@ -28,17 +37,12 @@ void findVersionByType() { .build() ); - // when then - var response = RestAssured - .given() - .when() - .get("/versions/" + version.getType()) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + mockMvc.perform( + get("/versions/" + version.getType()) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" { "id": %d, "version": "1.0.0", @@ -47,28 +51,24 @@ void findVersionByType() { "updated_at": "2024-01-15" } """, version.getId() - )); + ))); } @Test - @DisplayName("버전 타입을 통해 버전 정보를 조회한다. - 저장되지 않은 버전 타입을 요청한 경우 에러가 발생한다.") - void findVersionByTypeError() { + void 버전_타입을_통해_버전_정보를_조회한다_저장되지_않은_버전_타입을_요청한_경우_에러가_발생한다() throws Exception { VersionType failureType = VersionType.TIMETABLE; - RestAssured - .given() - .when() - .get("/versions/" + failureType.getValue()) - .then() - .statusCode(HttpStatus.NOT_FOUND.value()) - .extract(); - String undefinedType = "undefined"; - RestAssured - .given() - .when() - .get("/versions/" + undefinedType) - .then() - .statusCode(HttpStatus.NOT_FOUND.value()) - .extract(); + + mockMvc.perform( + get("/versions/" + failureType.getValue()) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isNotFound()); + + mockMvc.perform( + get("/versions/" + undefinedType) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isNotFound()); } } diff --git a/src/test/java/in/koreatech/koin/admin/acceptance/AdminCoopShopTest.java b/src/test/java/in/koreatech/koin/admin/acceptance/AdminCoopShopTest.java index 23168cc49..ff8998188 100644 --- a/src/test/java/in/koreatech/koin/admin/acceptance/AdminCoopShopTest.java +++ b/src/test/java/in/koreatech/koin/admin/acceptance/AdminCoopShopTest.java @@ -1,11 +1,13 @@ package in.koreatech.koin.admin.acceptance; -import static io.restassured.RestAssured.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; +import org.springframework.transaction.annotation.Transactional; import in.koreatech.koin.AcceptanceTest; import in.koreatech.koin.domain.coopshop.model.CoopShop; @@ -13,10 +15,10 @@ import in.koreatech.koin.domain.user.model.User; import in.koreatech.koin.fixture.CoopShopFixture; import in.koreatech.koin.fixture.UserFixture; -import in.koreatech.koin.support.JsonAssertions; -import io.restassured.RestAssured; @SuppressWarnings("NonAsciiCharacters") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Transactional class AdminCoopShopTest extends AcceptanceTest { @Autowired @@ -33,8 +35,9 @@ class AdminCoopShopTest extends AcceptanceTest { private User admin; private String token_admin; - @BeforeEach + @BeforeAll void setUp() { + clear(); 학생식당 = coopShopFixture.학생식당(); 세탁소 = coopShopFixture.세탁소(); admin = userFixture.코인_운영자(); @@ -42,23 +45,16 @@ void setUp() { } @Test - public void getCoopShops() { - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token_admin) - .when() - .param("page", 1) - .param("is_deleted", false) - .get("/admin/coopshop") - .then() - .log().all() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo( - """ - { + void 생협의_모든_상점을_조회한다() throws Exception { + mockMvc.perform( + get("/admin/coopshop") + .header("Authorization", "Bearer " + token_admin) + .param("page", "1") + .param("is_deleted", "false") + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { "totalCount": 2, "currentCount": 2, "totalPage": 1, @@ -124,25 +120,18 @@ public void getCoopShops() { } ] } - """ - ); + """)); } @Test - public void getCoopShop() { - var response = given() - .header("Authorization", "Bearer " + token_admin) - .when() - .get("/coopshop/2") - .then() - .log().all() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo( - """ - { + void 생협의_특정_상점을_조회한다() throws Exception { + mockMvc.perform( + get("/coopshop/2") + .header("Authorization", "Bearer " + token_admin) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { "id": 2, "name": "세탁소", "semester": "학기", @@ -164,8 +153,7 @@ public void getCoopShop() { "location": "학생회관 2층", "remarks": "연중무휴", "updated_at" : "2024-01-15" - } - """ - ); + } + """)); } } diff --git a/src/test/java/in/koreatech/koin/admin/acceptance/AdminLandApiTest.java b/src/test/java/in/koreatech/koin/admin/acceptance/AdminLandApiTest.java index cad868d9d..55e48daf8 100644 --- a/src/test/java/in/koreatech/koin/admin/acceptance/AdminLandApiTest.java +++ b/src/test/java/in/koreatech/koin/admin/acceptance/AdminLandApiTest.java @@ -2,14 +2,16 @@ import static org.assertj.core.api.SoftAssertions.assertSoftly; import static org.hibernate.validator.internal.util.Contracts.assertNotNull; -import static org.junit.Assert.assertEquals; -import static org.junit.jupiter.api.Assertions.assertAll; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import org.assertj.core.api.SoftAssertions; -import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.transaction.annotation.Transactional; import in.koreatech.koin.AcceptanceTest; import in.koreatech.koin.admin.land.repository.AdminLandRepository; @@ -17,10 +19,10 @@ import in.koreatech.koin.domain.user.model.User; import in.koreatech.koin.fixture.LandFixture; import in.koreatech.koin.fixture.UserFixture; -import in.koreatech.koin.support.JsonAssertions; -import io.restassured.RestAssured; @SuppressWarnings("NonAsciiCharacters") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Transactional class AdminLandApiTest extends AcceptanceTest { @Autowired @@ -32,9 +34,13 @@ class AdminLandApiTest extends AcceptanceTest { @Autowired private UserFixture userFixture; + @BeforeAll + void setup() { + clear(); + } + @Test - @DisplayName("관리자 권한으로 복덕방 목록을 검색한다.") - void getLands() { + void 관리자_권한으로_복덕방_목록을_검색한다() throws Exception { for (int i = 0; i < 11; i++) { Land request = Land.builder() .internalName("복덕방" + i) @@ -52,31 +58,22 @@ void getLands() { User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .param("page", 1) - .param("is_deleted", false) - .get("/admin/lands") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - assertSoftly( - softly -> { - softly.assertThat(response.body().jsonPath().getInt("total_count")).isEqualTo(11); - softly.assertThat(response.body().jsonPath().getInt("current_count")).isEqualTo(10); - softly.assertThat(response.body().jsonPath().getInt("total_page")).isEqualTo(2); - softly.assertThat(response.body().jsonPath().getInt("current_page")).isEqualTo(1); - softly.assertThat(response.body().jsonPath().getList("lands").size()).isEqualTo(10); - } - ); + mockMvc.perform( + get("/admin/lands") + .header("Authorization", "Bearer " + token) + .param("page", "1") + .param("is_deleted", "false") + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.total_count").value(11)) + .andExpect(jsonPath("$.current_count").value(10)) + .andExpect(jsonPath("$.total_page").value(2)) + .andExpect(jsonPath("$.current_page").value(1)) + .andExpect(jsonPath("$.lands.length()").value(10)); } @Test - @DisplayName("관리자 권한으로 복덕방을 추가한다.") - void postLands() { + void 관리자_권한으로_복덕방을_추가한다() throws Exception { String jsonBody = """ { "name": "금실타운", @@ -108,16 +105,13 @@ void postLands() { User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType("application/json") - .body(jsonBody) - .when() - .post("/admin/lands") - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract().asString(); + mockMvc.perform( + post("/admin/lands") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonBody) + ) + .andExpect(status().isCreated()); Land savedLand = adminLandRepository.getByName("금실타운"); assertNotNull(savedLand); @@ -132,8 +126,7 @@ void postLands() { } @Test - @DisplayName("관리자 권한으로 복덕방을 삭제한다.") - void deleteLand() { + void 관리자_권한으로_복덕방을_삭제한다() throws Exception { // 복덕방 생성 Land request = Land.builder() .internalName("금실타운") @@ -151,14 +144,11 @@ void deleteLand() { User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .delete("/admin/lands/{id}", landId) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + mockMvc.perform( + delete("/admin/lands/{id}", landId) + .header("Authorization", "Bearer " + token) + ) + .andExpect(status().isOk()); Land deletedLand = adminLandRepository.getById(landId); @@ -169,8 +159,7 @@ void deleteLand() { } @Test - @DisplayName("관리자의 권한으로 특정 복덕방 정보를 조회한다.") - void getLand() { + void 관리자의_권한으로_특정_복덕방_정보를_조회한다() throws Exception { // 복덕방 생성 Land request = Land.builder() .internalName("금실타운") @@ -191,58 +180,52 @@ void getLand() { User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .get("/admin/lands/{id}", landId) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" - { - "id": %d, - "name": "금실타운", - "internal_name": "금실타운", - "size": 9.0, - "room_type": "원룸", - "latitude": 37.555, - "longitude": 126.555, - "phone": null, - "image_urls": [], - "address": "가전리 123", - "description": "테스트용 복덕방", - "floor": null, - "deposit": null, - "monthly_fee": "100", - "charter_fee": "1000", - "management_fee": null, - "opt_closet": false, - "opt_tv": false, - "opt_microwave": false, - "opt_gas_range": false, - "opt_induction": false, - "opt_water_purifier": false, - "opt_air_conditioner": false, - "opt_washer": false, - "opt_bed": false, - "opt_bidet": false, - "opt_desk": false, - "opt_electronic_door_locks": false, - "opt_elevator": false, - "opt_refrigerator": false, - "opt_shoe_closet": false, - "opt_veranda": false, - "is_deleted": false - } - """, landId)); + mockMvc.perform( + get("/admin/lands/{id}", landId) + .header("Authorization", "Bearer " + token) + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" + { + "id": %d, + "name": "금실타운", + "internal_name": "금실타운", + "size": 9.0, + "room_type": "원룸", + "latitude": 37.555, + "longitude": 126.555, + "phone": null, + "image_urls": [], + "address": "가전리 123", + "description": "테스트용 복덕방", + "floor": null, + "deposit": null, + "monthly_fee": "100", + "charter_fee": "1000", + "management_fee": null, + "opt_closet": false, + "opt_tv": false, + "opt_microwave": false, + "opt_gas_range": false, + "opt_induction": false, + "opt_water_purifier": false, + "opt_air_conditioner": false, + "opt_washer": false, + "opt_bed": false, + "opt_bidet": false, + "opt_desk": false, + "opt_electronic_door_locks": false, + "opt_elevator": false, + "opt_refrigerator": false, + "opt_shoe_closet": false, + "opt_veranda": false, + "is_deleted": false + } + """, landId))); } @Test - @DisplayName("관리자 권한으로 복덕방 정보를 수정한다.") - void updateLand() { + void 관리자_권한으로_복덕방_정보를_수정한다() throws Exception { Land land = landFixture.신안빌(); Integer landId = land.getId(); @@ -277,16 +260,13 @@ void updateLand() { } """; - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType("application/json") - .body(jsonBody) - .when() - .put("/admin/lands/{id}", landId) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + mockMvc.perform( + put("/admin/lands/{id}", landId) + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonBody) + ) + .andExpect(status().isOk()); Land updatedLand = adminLandRepository.getById(landId); @@ -319,22 +299,18 @@ void updateLand() { } @Test - @DisplayName("관리자 권한으로 복덕방 삭제를 취소한다.") - void undeleteLand() { + void 관리자_권한으로_복덕방_삭제를_취소한다() throws Exception { Land deletedLand = landFixture.삭제된_복덕방(); Integer landId = deletedLand.getId(); User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .post("/admin/lands/{id}/undelete", landId) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + mockMvc.perform( + post("/admin/lands/{id}/undelete", landId) + .header("Authorization", "Bearer " + token) + ) + .andExpect(status().isOk()); Land undeletedLand = adminLandRepository.getById(landId); @@ -344,5 +320,4 @@ void undeleteLand() { softly.assertThat(undeletedLand.isDeleted()).isFalse(); }); } - } diff --git a/src/test/java/in/koreatech/koin/admin/acceptance/AdminMemberApiTest.java b/src/test/java/in/koreatech/koin/admin/acceptance/AdminMemberApiTest.java index d65b45b6e..e706914ee 100644 --- a/src/test/java/in/koreatech/koin/admin/acceptance/AdminMemberApiTest.java +++ b/src/test/java/in/koreatech/koin/admin/acceptance/AdminMemberApiTest.java @@ -1,12 +1,15 @@ package in.koreatech.koin.admin.acceptance; -import static org.hibernate.validator.internal.util.Contracts.assertNotNull; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import org.assertj.core.api.SoftAssertions; -import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.transaction.annotation.Transactional; import in.koreatech.koin.AcceptanceTest; import in.koreatech.koin.admin.member.repository.AdminMemberRepository; @@ -15,10 +18,10 @@ import in.koreatech.koin.fixture.MemberFixture; import in.koreatech.koin.fixture.TrackFixture; import in.koreatech.koin.fixture.UserFixture; -import in.koreatech.koin.support.JsonAssertions; -import io.restassured.RestAssured; @SuppressWarnings("NonAsciiCharacters") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Transactional public class AdminMemberApiTest extends AcceptanceTest { @Autowired @@ -33,28 +36,27 @@ public class AdminMemberApiTest extends AcceptanceTest { @Autowired private AdminMemberRepository adminMemberRepository; + @BeforeAll + void setup() { + clear(); + } + @Test - @DisplayName("BCSDLab 회원들의 정보를 조회한다") - void getMembers() { + void BCSDLab_회원들의_정보를_조회한다() throws Exception { memberFixture.최준호(trackFixture.backend()); User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .param("page", 1) - .param("track", "BACKEND") - .param("is_deleted", false) - .get("/admin/members") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/admin/members") + .header("Authorization", "Bearer " + token) + .param("page", "1") + .param("track", "BACKEND") + .param("is_deleted", "false") + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "total_count": 1, "current_count": 1, @@ -73,13 +75,11 @@ void getMembers() { } ] } - """ - ); + """)); } @Test - @DisplayName("관리자 권한으로 BCSDLab 회원을 추가한다.") - void postMember() { + void 관리자_권한으로_BCSDLab_회원을_추가한다() throws Exception { trackFixture.backend(); User adminUser = userFixture.코인_운영자(); @@ -96,16 +96,13 @@ void postMember() { } """; - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType("application/json") - .body(jsonBody) - .when() - .post("/admin/members") - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract().asString(); + mockMvc.perform( + post("/admin/members") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonBody) + ) + .andExpect(status().isCreated()); Member savedMember = adminMemberRepository.getByName("최준호"); @@ -121,24 +118,18 @@ void postMember() { } @Test - @DisplayName("BCSDLab 회원 정보를 조회한다") - void getMember() { + void BCSDLab_회원_정보를_조회한다() throws Exception { memberFixture.최준호(trackFixture.backend()); User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .get("/admin/members/{id}", 1) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/admin/members/{id}", 1) + .header("Authorization", "Bearer " + token) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "id": 1, "name": "최준호", @@ -149,27 +140,22 @@ void getMember() { "image_url": "https://imagetest.com/juno.jpg", "is_deleted": false } - """ - ); + """)); } @Test - @DisplayName("BCSDLab 회원 정보를 삭제한다") - void deleteMember() { + void BCSDLab_회원_정보를_삭제한다() throws Exception { Member member = memberFixture.최준호(trackFixture.backend()); Integer memberId = member.getId(); User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .delete("/admin/members/{id}", memberId) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + mockMvc.perform( + delete("/admin/members/{id}", memberId) + .header("Authorization", "Bearer " + token) + ) + .andExpect(status().isOk()); Member savedMember = adminMemberRepository.getById(memberId); @@ -185,8 +171,7 @@ void deleteMember() { } @Test - @DisplayName("BCSDLab 회원 정보를 수정한다") - void updateMember() { + void BCSDLab_회원_정보를_수정한다() throws Exception { Member member = memberFixture.최준호(trackFixture.backend()); Integer memberId = member.getId(); @@ -204,16 +189,13 @@ void updateMember() { } """; - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType("application/json") - .body(jsonBody) - .when() - .put("/admin/members/{id}", memberId) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + mockMvc.perform( + put("/admin/members/{id}", memberId) + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonBody) + ) + .andExpect(status().isOk()); Member updatedMember = adminMemberRepository.getById(memberId); @@ -229,8 +211,7 @@ void updateMember() { } @Test - @DisplayName("BCSDLab 회원 정보를 트랙과 함께 수정한다") - void updateMemberWithTrack() { + void BCSDLab_회원_정보를_트랙과_함께_수정한다() throws Exception { Member member = memberFixture.최준호(trackFixture.backend()); trackFixture.frontend(); Integer memberId = member.getId(); @@ -249,16 +230,13 @@ void updateMemberWithTrack() { } """; - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType("application/json") - .body(jsonBody) - .when() - .put("/admin/members/{id}", memberId) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + mockMvc.perform( + put("/admin/members/{id}", memberId) + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(jsonBody) + ) + .andExpect(status().isOk()); Member updatedMember = adminMemberRepository.getById(memberId); @@ -274,22 +252,18 @@ void updateMemberWithTrack() { } @Test - @DisplayName("BCSDLab 회원 정보를 삭제를 취소한다") - void undeleteMember() { + void BCSDLab_회원_정보를_삭제를_취소한다() throws Exception { Member member = memberFixture.최준호_삭제(trackFixture.backend()); Integer memberId = member.getId(); User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .post("/admin/members/{id}/undelete", memberId) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + mockMvc.perform( + post("/admin/members/{id}/undelete", memberId) + .header("Authorization", "Bearer " + token) + ) + .andExpect(status().isOk()); Member savedMember = adminMemberRepository.getById(memberId); diff --git a/src/test/java/in/koreatech/koin/admin/acceptance/AdminShopApiTest.java b/src/test/java/in/koreatech/koin/admin/acceptance/AdminShopApiTest.java index 41f5e0a11..13e8ceaa0 100644 --- a/src/test/java/in/koreatech/koin/admin/acceptance/AdminShopApiTest.java +++ b/src/test/java/in/koreatech/koin/admin/acceptance/AdminShopApiTest.java @@ -2,15 +2,19 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import java.util.List; import java.util.Set; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; import in.koreatech.koin.AcceptanceTest; @@ -35,13 +39,16 @@ import in.koreatech.koin.fixture.ShopCategoryFixture; import in.koreatech.koin.fixture.ShopFixture; import in.koreatech.koin.fixture.UserFixture; -import in.koreatech.koin.support.JsonAssertions; -import io.restassured.RestAssured; -import io.restassured.http.ContentType; +import jakarta.persistence.EntityManager; @SuppressWarnings("NonAsciiCharacters") +@Transactional +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class AdminShopApiTest extends AcceptanceTest { + @Autowired + EntityManager entityManager; + @Autowired private TransactionTemplate transactionTemplate; @@ -82,8 +89,9 @@ class AdminShopApiTest extends AcceptanceTest { private MenuCategory menuCategory_메인; private MenuCategory menuCategory_사이드; - @BeforeEach + @BeforeAll void setUp() { + clear(); admin = userFixture.코인_운영자(); token_admin = userFixture.getToken(admin); owner_현수 = userFixture.현수_사장님(); @@ -96,8 +104,7 @@ void setUp() { } @Test - @DisplayName("어드민이 모든 상점을 조회한다.") - void findAllShops() { + void 어드민이_모든_상점을_조회한다() throws Exception { for (int i = 0; i < 12; i++) { Shop request = shopFixture.builder() .owner(owner_현수) @@ -118,42 +125,28 @@ void findAllShops() { adminShopRepository.save(request); } - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token_admin) - .when() - .param("page", 1) - .param("is_deleted", false) - .get("/admin/shops") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - assertSoftly( - softly -> { - softly.assertThat(response.body().jsonPath().getInt("total_count")).isEqualTo(13); - softly.assertThat(response.body().jsonPath().getInt("current_count")).isEqualTo(10); - softly.assertThat(response.body().jsonPath().getInt("total_page")).isEqualTo(2); - softly.assertThat(response.body().jsonPath().getInt("current_page")).isEqualTo(1); - softly.assertThat(response.body().jsonPath().getList("shops").size()).isEqualTo(10); - } - ); + mockMvc.perform( + get("/admin/shops") + .header("Authorization", "Bearer " + token_admin) + .param("page", "1") + .param("is_deleted", "false") + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.total_count").value(13)) + .andExpect(jsonPath("$.current_count").value(10)) + .andExpect(jsonPath("$.total_page").value(2)) + .andExpect(jsonPath("$.current_page").value(1)) + .andExpect(jsonPath("$.shops.length()").value(10)); } @Test - @DisplayName("어드민이 특정 상점을 조회한다.") - void findShop() { - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token_admin) - .when() - .get("/admin/shops/{shopId}", shop_마슬랜.getId()) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + void 어드민이_특정_상점을_조회한다() throws Exception { + mockMvc.perform( + get("/admin/shops/{shopId}", shop_마슬랜.getId()) + .header("Authorization", "Bearer " + token_admin) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "address": "천안시 동남구 병천면 1600", "delivery": true, @@ -201,85 +194,62 @@ void findShop() { "bank": "국민", "account_number": "01022595923" } - """); + """)); } @Test - @DisplayName("어드민이 상점의 모든 카테고리를 조회한다.") - void findShopCategories() { + void 어드민이_상점의_모든_카테고리를_조회한다() throws Exception { for (int i = 0; i < 12; i++) { ShopCategory request = ShopCategory.builder() .name("카테고리" + i) .isDeleted(false) .build(); adminShopCategoryRepository.save(request); - System.out.println(i); } - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token_admin) - .when() - .param("page", 1) - .param("is_deleted", false) - .get("/admin/shops/categories") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - assertSoftly( - softly -> { - softly.assertThat(response.body().jsonPath().getInt("total_count")).isEqualTo(14); - softly.assertThat(response.body().jsonPath().getInt("current_count")).isEqualTo(10); - softly.assertThat(response.body().jsonPath().getInt("total_page")).isEqualTo(2); - softly.assertThat(response.body().jsonPath().getInt("current_page")).isEqualTo(1); - softly.assertThat(response.body().jsonPath().getList("categories").size()).isEqualTo(10); - } - ); + mockMvc.perform( + get("/admin/shops/categories") + .header("Authorization", "Bearer " + token_admin) + .param("page", "1") + .param("is_deleted", "false") + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.total_count").value(14)) + .andExpect(jsonPath("$.current_count").value(10)) + .andExpect(jsonPath("$.total_page").value(2)) + .andExpect(jsonPath("$.current_page").value(1)) + .andExpect(jsonPath("$.categories.length()").value(10)); } @Test - @DisplayName("어드민이 상점의 특정 카테고리를 조회한다.") - void findShopCategory() { + void 어드민이_상점의_특정_카테고리를_조회한다() throws Exception { ShopCategory shopCategory = shopCategoryFixture.카테고리_치킨(); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token_admin) - .pathParam("id", shopCategory.getId()) - .when() - .get("/admin/shops/categories/{id}") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/admin/shops/categories/{id}", shopCategory.getId()) + .header("Authorization", "Bearer " + token_admin) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "id": 3, "image_url": "https://test-image.com/ckicken.jpg", "name": "치킨" } - """); + """)); } @Test - @DisplayName("어드민이 특정 상점의 모든 메뉴를 조회한다.") - void findShopMenus() { + void 어드민이_특정_상점의_모든_메뉴를_조회한다() throws Exception { // given menuFixture.짜장면_옵션메뉴(shop_마슬랜, menuCategory_메인); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token_admin) - .pathParam("id", shop_마슬랜.getId()) - .when() - .get("/admin/shops/{id}/menus") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + + mockMvc.perform( + get("/admin/shops/{id}/menus", shop_마슬랜.getId()) + .header("Authorization", "Bearer " + token_admin) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "count": 1, "menu_categories": [ @@ -314,25 +284,20 @@ void findShopMenus() { ], "updated_at": "2024-01-15" } - """); + """)); } @Test - @DisplayName("어드민이 특정 상점의 메뉴 카테고리들을 조회한다.") - void findShopMenuCategories() { + void 어드민이_특정_상점의_메뉴_카테고리들을_조회한다() throws Exception { // given menuFixture.짜장면_단일메뉴(shop_마슬랜, menuCategory_메인); - var response = RestAssured - .given() - .pathParam("id", shop_마슬랜.getId()) - .header("Authorization", "Bearer " + token_admin) - .when() - .get("/admin/shops/{id}/menus/categories") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + + mockMvc.perform( + get("/admin/shops/{id}/menus/categories", shop_마슬랜.getId()) + .header("Authorization", "Bearer " + token_admin) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "count": 2, "menu_categories": [ @@ -346,27 +311,20 @@ void findShopMenuCategories() { } ] } - """); + """)); } @Test - @DisplayName("어드민이 특정 상점의 특정 메뉴를 조회한다.") - void findShopMenu() { + void 어드민이_특정_상점의_특정_메뉴를_조회한다() throws Exception { // given Menu menu = menuFixture.짜장면_옵션메뉴(shop_마슬랜, menuCategory_메인); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token_admin) - .pathParam("shopId", shop_마슬랜.getId()) - .pathParam("menuId", menu.getId()) - .when() - .get("/admin/shops/{shopId}/menus/{menuId}", menu.getId()) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - System.out.println(JsonAssertions.assertThat(response.asPrettyString())); - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + + mockMvc.perform( + get("/admin/shops/{shopId}/menus/{menuId}", menu.getId(), shop_마슬랜.getId()) + .header("Authorization", "Bearer " + token_admin) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "category_ids": [1], "description": "맛있는 짜장면", @@ -391,122 +349,108 @@ void findShopMenu() { "shop_id": 1, "single_price": null } - """); + """)); } @Test - @DisplayName("어드민이 상점을 생성한다.") - void createShop() { - RestAssured - .given() - .header("Authorization", "Bearer " + token_admin) - .contentType(ContentType.JSON) - .body(String.format(""" - { - "address": "대전광역시 유성구 대학로 291", - "category_ids": [ - %d - ], - "delivery": true, - "delivery_price": 4000, - "description": "테스트 상점2입니다.", - "image_urls": [ - "https://test.com/test1.jpg", - "https://test.com/test2.jpg", - "https://test.com/test3.jpg" - ], - "name": "테스트 상점2", - "open": [ - { - "close_time": "21:00", - "closed": false, - "day_of_week": "MONDAY", - "open_time": "09:00" - }, - { - "close_time": "21:00", - "closed": false, - "day_of_week": "TUESDAY", - "open_time": "09:00" - }, - { - "close_time": "21:00", - "closed": false, - "day_of_week": "WEDNESDAY", - "open_time": "09:00" - }, - { - "close_time": "21:00", - "closed": false, - "day_of_week": "THURSDAY", - "open_time": "09:00" - }, - { - "close_time": "21:00", - "closed": false, - "day_of_week": "FRIDAY", - "open_time": "09:00" - }, - { - "close_time": "21:00", - "closed": false, - "day_of_week": "SATURDAY", - "open_time": "09:00" - }, + void 어드민이_상점을_생성한다() throws Exception { + mockMvc.perform( + post("/admin/shops") + .header("Authorization", "Bearer " + token_admin) + .contentType(MediaType.APPLICATION_JSON) + .content(String.format(""" { - "close_time": "21:00", - "closed": false, - "day_of_week": "SUNDAY", - "open_time": "09:00" + "address": "대전광역시 유성구 대학로 291", + "category_ids": [ + %d + ], + "delivery": true, + "delivery_price": 4000, + "description": "테스트 상점2입니다.", + "image_urls": [ + "https://test.com/test1.jpg", + "https://test.com/test2.jpg", + "https://test.com/test3.jpg" + ], + "name": "테스트 상점2", + "open": [ + { + "close_time": "21:00", + "closed": false, + "day_of_week": "MONDAY", + "open_time": "09:00" + }, + { + "close_time": "21:00", + "closed": false, + "day_of_week": "TUESDAY", + "open_time": "09:00" + }, + { + "close_time": "21:00", + "closed": false, + "day_of_week": "WEDNESDAY", + "open_time": "09:00" + }, + { + "close_time": "21:00", + "closed": false, + "day_of_week": "THURSDAY", + "open_time": "09:00" + }, + { + "close_time": "21:00", + "closed": false, + "day_of_week": "FRIDAY", + "open_time": "09:00" + }, + { + "close_time": "21:00", + "closed": false, + "day_of_week": "SATURDAY", + "open_time": "09:00" + }, + { + "close_time": "21:00", + "closed": false, + "day_of_week": "SUNDAY", + "open_time": "09:00" + } + ], + "pay_bank": true, + "pay_card": true, + "phone": "010-1234-5678" } - ], - "pay_bank": true, - "pay_card": true, - "phone": "010-1234-5678" - } - """, shopCategory_치킨.getId()) + """, shopCategory_치킨.getId())) ) - .when() - .post("/admin/shops") - .then() - .log().all() - .statusCode(HttpStatus.CREATED.value()) - .extract(); - - transactionTemplate.executeWithoutResult(status -> { - Shop result = adminShopRepository.getById(2); - assertSoftly( - softly -> { - softly.assertThat(result.getAddress()).isEqualTo("대전광역시 유성구 대학로 291"); - softly.assertThat(result.getDeliveryPrice()).isEqualTo(4000); - softly.assertThat(result.getDescription()).isEqualTo("테스트 상점2입니다."); - softly.assertThat(result.getName()).isEqualTo("테스트 상점2"); - softly.assertThat(result.getShopImages()).hasSize(3); - softly.assertThat(result.getShopOpens()).hasSize(7); - softly.assertThat(result.getShopCategories()).hasSize(1); - } - ); + .andExpect(status().isCreated()); + + Shop savedShop = adminShopRepository.getById(2); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(savedShop.getAddress()).isEqualTo("대전광역시 유성구 대학로 291"); + softly.assertThat(savedShop.getDeliveryPrice()).isEqualTo(4000); + softly.assertThat(savedShop.getDescription()).isEqualTo("테스트 상점2입니다."); + softly.assertThat(savedShop.getName()).isEqualTo("테스트 상점2"); + softly.assertThat(savedShop.getShopImages()).hasSize(3); + softly.assertThat(savedShop.getShopOpens()).hasSize(7); + softly.assertThat(savedShop.getShopCategories()).hasSize(1); }); } @Test - @DisplayName("어드민이 상점 카테고리를 생성한다.") - void createShopCategory() { - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token_admin) - .contentType(ContentType.JSON) - .body(""" - { - "image_url": "https://image.png", - "name": "새로운 카테고리" - } - """) - .when() - .post("/admin/shops/categories") - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); + void 어드민이_상점_카테고리를_생성한다() throws Exception { + mockMvc.perform( + post("/admin/shops/categories") + .header("Authorization", "Bearer " + token_admin) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "image_url": "https://image.png", + "name": "새로운 카테고리" + } + """) + ) + .andExpect(status().isCreated()); transactionTemplate.executeWithoutResult(status -> { ShopCategory result = adminShopCategoryRepository.getById(3); @@ -521,128 +465,114 @@ void createShopCategory() { } @Test - @DisplayName("어드민이 옵션이 여러개인 메뉴를 추가한다.") - void createManyOptionMenu() { + void 어드민이_옵션이_여러개인_메뉴를_추가한다() throws Exception { // given MenuCategory menuCategory = menuCategory_메인; - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token_admin) - .contentType(ContentType.JSON) - .body(String.format(""" - { - "category_ids": [ - %s - ], - "description": "테스트메뉴입니다.", - "image_urls": [ - "https://test-image.com/짜장면.jpg" - ], - "is_single": false, - "name": "짜장면", - "option_prices": [ - { - "option": "중", - "price": 10000 - }, - { - "option": "소", - "price": 5000 - } - ] - } - """, menuCategory.getId())) - .when() - .post("/admin/shops/{id}/menus", shop_마슬랜.getId()) - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); - transactionTemplate.executeWithoutResult(status -> { - Menu menu = adminMenuRepository.getById(1); - assertSoftly( - softly -> { - List menuCategoryMaps = menu.getMenuCategoryMaps(); - List menuOptions = menu.getMenuOptions(); - List menuImages = menu.getMenuImages(); - softly.assertThat(menu.getDescription()).isEqualTo("테스트메뉴입니다."); - softly.assertThat(menu.getName()).isEqualTo("짜장면"); - softly.assertThat(menuImages.get(0).getImageUrl()).isEqualTo("https://test-image.com/짜장면.jpg"); - softly.assertThat(menuCategoryMaps.get(0).getMenuCategory().getId()).isEqualTo(1); - softly.assertThat(menuOptions).hasSize(2); - } - ); - }); + mockMvc.perform( + post("/admin/shops/{id}/menus", shop_마슬랜.getId()) + .header("Authorization", "Bearer " + token_admin) + .contentType(MediaType.APPLICATION_JSON) + .content(String.format(""" + { + "category_ids": [ + %s + ], + "description": "테스트메뉴입니다.", + "image_urls": [ + "https://test-image.com/짜장면.jpg" + ], + "is_single": false, + "name": "짜장면", + "option_prices": [ + { + "option": "중", + "price": 10000 + }, + { + "option": "소", + "price": 5000 + } + ] + } + """, menuCategory.getId())) + ) + .andExpect(status().isCreated()); + + Menu menu = adminMenuRepository.getById(1); + assertSoftly( + softly -> { + List menuCategoryMaps = menu.getMenuCategoryMaps(); + List menuOptions = menu.getMenuOptions(); + List menuImages = menu.getMenuImages(); + softly.assertThat(menu.getDescription()).isEqualTo("테스트메뉴입니다."); + softly.assertThat(menu.getName()).isEqualTo("짜장면"); + softly.assertThat(menuImages.get(0).getImageUrl()).isEqualTo("https://test-image.com/짜장면.jpg"); + softly.assertThat(menuCategoryMaps.get(0).getMenuCategory().getId()).isEqualTo(1); + softly.assertThat(menuOptions).hasSize(2); + } + ); } @Test - @DisplayName("어드민이 옵션이 한개인 메뉴를 추가한다.") - void createOneOptionMenu() { + void 어드민이_옵션이_한개인_메뉴를_추가한다() throws Exception { // given MenuCategory menuCategory = menuCategory_메인; - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token_admin) - .contentType(ContentType.JSON) - .body(String.format(""" - { - "category_ids": [ - %s - ], - "description": "테스트메뉴입니다.", - "image_urls": [ - "https://test-image.com/짜장면.jpg" - ], - "is_single": true, - "name": "짜장면", - "option_prices": null, - "single_price": 10000 - } - """, menuCategory.getId())) - .when() - .post("/admin/shops/{id}/menus", shop_마슬랜.getId()) - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); - transactionTemplate.executeWithoutResult(status -> { - Menu menu = adminMenuRepository.getById(1); - assertSoftly( - softly -> { - List menuCategoryMaps = menu.getMenuCategoryMaps(); - List menuOptions = menu.getMenuOptions(); - List menuImages = menu.getMenuImages(); - softly.assertThat(menu.getDescription()).isEqualTo("테스트메뉴입니다."); - softly.assertThat(menu.getName()).isEqualTo("짜장면"); + mockMvc.perform( + post("/admin/shops/{id}/menus", shop_마슬랜.getId()) + .header("Authorization", "Bearer " + token_admin) + .contentType(MediaType.APPLICATION_JSON) + .content(String.format(""" + { + "category_ids": [ + %s + ], + "description": "테스트메뉴입니다.", + "image_urls": [ + "https://test-image.com/짜장면.jpg" + ], + "is_single": true, + "name": "짜장면", + "option_prices": null, + "single_price": 10000 + } + """, menuCategory.getId())) + ) + .andExpect(status().isCreated()); - softly.assertThat(menuImages.get(0).getImageUrl()).isEqualTo("https://test-image.com/짜장면.jpg"); - softly.assertThat(menuCategoryMaps.get(0).getMenuCategory().getId()).isEqualTo(1); + Menu menu = adminMenuRepository.getById(1); + assertSoftly( + softly -> { + List menuCategoryMaps = menu.getMenuCategoryMaps(); + List menuOptions = menu.getMenuOptions(); + List menuImages = menu.getMenuImages(); + System.out.println("transaction test"); + softly.assertThat(menu.getDescription()).isEqualTo("테스트메뉴입니다."); + softly.assertThat(menu.getName()).isEqualTo("짜장면"); - softly.assertThat(menuOptions.get(0).getPrice()).isEqualTo(10000); - } - ); - }); + softly.assertThat(menuImages.get(0).getImageUrl()).isEqualTo("https://test-image.com/짜장면.jpg"); + softly.assertThat(menuCategoryMaps.get(0).getMenuCategory().getId()).isEqualTo(1); + + softly.assertThat(menuOptions.get(0).getPrice()).isEqualTo(10000); + } + ); } @Test - @DisplayName("어드민이 메뉴 카테고리를 추가한다.") - void createMenuCategory() { + void 어드민이_메뉴_카테고리를_추가한다() throws Exception { // given - RestAssured - .given() - .header("Authorization", "Bearer " + token_admin) - .pathParam("id", shop_마슬랜.getId()) - .contentType(ContentType.JSON) - .body(String.format(""" - { - "name": "대박메뉴" - } - """)) - .when() - .post("/admin/shops/{id}/menus/categories") - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); + mockMvc.perform( + post("/admin/shops/{id}/menus/categories", shop_마슬랜.getId()) + .header("Authorization", "Bearer " + token_admin) + .contentType(MediaType.APPLICATION_JSON) + .content(String.format(""" + { + "name": "대박메뉴" + } + """)) + ) + .andExpect(status().isCreated()); var menuCategories = adminMenuCategoryRepository.findAllByShopId(shop_마슬랜.getId()); @@ -650,100 +580,90 @@ void createMenuCategory() { } @Test - @DisplayName("어드민이 상점 삭제를 해제한다.") - void cancelShopDeleted() { + void 어드민이_상점_삭제를_해제한다() throws Exception { // given - System.out.println("qwe"); adminShopRepository.deleteById(shop_마슬랜.getId()); - RestAssured - .given() - .header("Authorization", "Bearer " + token_admin) - .pathParam("id", shop_마슬랜.getId()) - .contentType(ContentType.JSON) - .when() - .post("/admin/shops/{id}/undelete") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + + mockMvc.perform( + post("/admin/shops/{id}/undelete", shop_마슬랜.getId()) + .header("Authorization", "Bearer " + token_admin) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()); var shop = adminShopRepository.getById(shop_마슬랜.getId()); assertSoftly(softly -> softly.assertThat(shop.isDeleted()).isFalse()); } @Test - @DisplayName("어드민이 상점을 수정한다.") - void modifyShop() { - RestAssured - .given() - .header("Authorization", "Bearer " + token_admin) - .contentType(ContentType.JSON) - .body(String.format(""" - { - "address": "충청남도 천안시 동남구 병천면 충절로 1600", - "category_ids": [ - %d, %d - ], - "delivery": false, - "delivery_price": 1000, - "description": "이번주 전 메뉴 10%% 할인 이벤트합니다.", - "image_urls": [ - "https://fixed-shopimage.com/수정된_상점_이미지.png" - ], - "name": "써니 숯불 도시락", - "open": [ - { - "close_time": "00:00", - "closed": false, - "day_of_week": "MONDAY", - "open_time": "01:00" - }, - { - "close_time": "21:00", - "closed": false, - "day_of_week": "TUESDAY", - "open_time": "09:00" - }, - { - "close_time": "21:00", - "closed": false, - "day_of_week": "WEDNESDAY", - "open_time": "09:00" - }, - { - "close_time": "21:00", - "closed": false, - "day_of_week": "THURSDAY", - "open_time": "09:00" - }, - { - "close_time": "21:00", - "closed": false, - "day_of_week": "FRIDAY", - "open_time": "09:00" - }, - { - "close_time": "21:00", - "closed": false, - "day_of_week": "SATURDAY", - "open_time": "09:00" - }, - { - "close_time": "21:00", - "closed": false, - "day_of_week": "SUNDAY", - "open_time": "09:00" - } - ], - "pay_bank": true, - "pay_card": true, - "phone": "041-123-4567" - } - """, shopCategory_일반.getId(), shopCategory_치킨.getId() - )) - .when() - .put("/admin/shops/{id}", shop_마슬랜.getId()) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + void 어드민이_상점을_수정한다() throws Exception { + mockMvc.perform( + put("/admin/shops/{id}", shop_마슬랜.getId()) + .header("Authorization", "Bearer " + token_admin) + .contentType(MediaType.APPLICATION_JSON) + .content(String.format(""" + { + "address": "충청남도 천안시 동남구 병천면 충절로 1600", + "category_ids": [ + %d, %d + ], + "delivery": false, + "delivery_price": 1000, + "description": "이번주 전 메뉴 10%% 할인 이벤트합니다.", + "image_urls": [ + "https://fixed-shopimage.com/수정된_상점_이미지.png" + ], + "name": "써니 숯불 도시락", + "open": [ + { + "close_time": "00:00", + "closed": false, + "day_of_week": "MONDAY", + "open_time": "01:00" + }, + { + "close_time": "21:00", + "closed": false, + "day_of_week": "TUESDAY", + "open_time": "09:00" + }, + { + "close_time": "21:00", + "closed": false, + "day_of_week": "WEDNESDAY", + "open_time": "09:00" + }, + { + "close_time": "21:00", + "closed": false, + "day_of_week": "THURSDAY", + "open_time": "09:00" + }, + { + "close_time": "21:00", + "closed": false, + "day_of_week": "FRIDAY", + "open_time": "09:00" + }, + { + "close_time": "21:00", + "closed": false, + "day_of_week": "SATURDAY", + "open_time": "09:00" + }, + { + "close_time": "21:00", + "closed": false, + "day_of_week": "SUNDAY", + "open_time": "09:00" + } + ], + "pay_bank": true, + "pay_card": true, + "phone": "041-123-4567" + } + """, shopCategory_일반.getId(), shopCategory_치킨.getId())) + ) + .andExpect(status().isOk()); transactionTemplate.executeWithoutResult(status -> { Shop result = adminShopRepository.getById(shop_마슬랜.getId()); @@ -778,26 +698,21 @@ void modifyShop() { @Test - @DisplayName("어드민이 상점 카테고리를 수정한다.") - void modifyShopCategory() { + void 어드민이_상점_카테고리를_수정한다() throws Exception { ShopCategory shopCategory = shopCategoryFixture.카테고리_일반음식(); - RestAssured - .given() - .header("Authorization", "Bearer " + token_admin) - .contentType(ContentType.JSON) - .pathParam("id", shopCategory.getId()) - .body(""" - { - "image_url": "http://image.png", - "name": "수정된 카테고리 이름" - } - """) - .when() - .put("/admin/shops/categories/{id}") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + mockMvc.perform( + put("/admin/shops/categories/{id}", shopCategory.getId()) + .header("Authorization", "Bearer " + token_admin) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "image_url": "http://image.png", + "name": "수정된 카테고리 이름" + } + """) + ) + .andExpect(status().isOk()); transactionTemplate.executeWithoutResult(status -> { ShopCategory updatedCategory = adminShopCategoryRepository.getById(shopCategory.getId()); @@ -812,62 +727,51 @@ void modifyShopCategory() { } @Test - @DisplayName("어드민이 특점 상점의 메뉴 카테고리를 수정한다.") - void modifyMenuCategory() { + void 어드민이_특정_상점의_메뉴_카테고리를_수정한다() throws Exception { // given Menu menu = menuFixture.짜장면_단일메뉴(shop_마슬랜, menuCategory_메인); - RestAssured - .given() - .header("Authorization", "Bearer " + token_admin) - .contentType(ContentType.JSON) - .pathParam("shopId", shop_마슬랜.getId()) - .body(String.format(""" - { - "id": %s, - "name": "사이드 메뉴" - } - """, menuCategory_메인.getId())) - .when() - .put("/admin/shops/{shopId}/menus/categories") - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); + mockMvc.perform( + put("/admin/shops/{shopId}/menus/categories", shop_마슬랜.getId()) + .header("Authorization", "Bearer " + token_admin) + .contentType(MediaType.APPLICATION_JSON) + .content(String.format(""" + { + "id": %s, + "name": "사이드 메뉴" + } + """, menuCategory_메인.getId())) + ) + .andExpect(status().isCreated()); MenuCategory menuCategory = adminMenuCategoryRepository.getById(menuCategory_메인.getId()); assertSoftly(softly -> softly.assertThat(menuCategory.getName()).isEqualTo("사이드 메뉴")); } @Test - @DisplayName("어드민이 특정 삼점의 메뉴를 단일 메뉴로 수정한다.") - void modifyOneMenu() { + void 어드민이_특정_상점의_메뉴를_단일_메뉴로_수정한다() throws Exception { // given Menu menu = menuFixture.짜장면_단일메뉴(shop_마슬랜, menuCategory_메인); - RestAssured - .given() - .header("Authorization", "Bearer " + token_admin) - .contentType(ContentType.JSON) - .pathParam("shopId", shop_마슬랜.getId()) - .pathParam("menuId", menu.getId()) - .body(String.format(""" - { - "category_ids": [ - %d - ], - "description": "테스트메뉴수정", - "image_urls": [ - "https://test-image.net/테스트메뉴.jpeg" - ], - "is_single": true, - "name": "짜장면2", - "single_price": 10000 - } - """, shopCategory_일반.getId())) - .when() - .put("/admin/shops/{shopId}/menus/{menuId}") - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); + mockMvc.perform( + put("/admin/shops/{shopId}/menus/{menuId}", shop_마슬랜.getId(), menu.getId()) + .header("Authorization", "Bearer " + token_admin) + .contentType(MediaType.APPLICATION_JSON) + .content(String.format(""" + { + "category_ids": [ + %d + ], + "description": "테스트메뉴수정", + "image_urls": [ + "https://test-image.net/테스트메뉴.jpeg" + ], + "is_single": true, + "name": "짜장면2", + "single_price": 10000 + } + """, shopCategory_일반.getId())) + ) + .andExpect(status().isCreated()); transactionTemplate.executeWithoutResult(status -> { Menu result = adminMenuRepository.getById(1); @@ -890,45 +794,38 @@ void modifyOneMenu() { } @Test - @DisplayName("어드민이 특정 상점의 메뉴를 여러옵션을 가진 메뉴로 수정한다.") - void modifyManyOptionMenu() { + void 어드민이_특정_상점의_메뉴를_여러옵션을_가진_메뉴로_수정한다() throws Exception { // given Menu menu = menuFixture.짜장면_옵션메뉴(shop_마슬랜, menuCategory_메인); - RestAssured - .given() - .header("Authorization", "Bearer " + token_admin) - .pathParam("shopId", shop_마슬랜.getId()) - .pathParam("menuId", menu.getId()) - .contentType(ContentType.JSON) - .body(String.format(""" - { - "category_ids": [ - %d, %d - ], - "description": "테스트메뉴입니다.", - "image_urls": [ - "https://fixed-testimage.com/수정된짜장면.png" - ], - "is_single": false, - "name": "짜장면", - "option_prices": [ - { - "option": "중", - "price": 10000 - }, - { - "option": "소", - "price": 5000 - } - ] - } - """, menuCategory_메인.getId(), menuCategory_사이드.getId()) + mockMvc.perform( + put("/admin/shops/{shopId}/menus/{menuId}", shop_마슬랜.getId(), menu.getId()) + .header("Authorization", "Bearer " + token_admin) + .contentType(MediaType.APPLICATION_JSON) + .content(String.format(""" + { + "category_ids": [ + %d, %d + ], + "description": "테스트메뉴입니다.", + "image_urls": [ + "https://fixed-testimage.com/수정된짜장면.png" + ], + "is_single": false, + "name": "짜장면", + "option_prices": [ + { + "option": "중", + "price": 10000 + }, + { + "option": "소", + "price": 5000 + } + ] + } + """, menuCategory_메인.getId(), menuCategory_사이드.getId())) ) - .when() - .put("/admin/shops/{shopId}/menus/{menuId}") - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); + .andExpect(status().isCreated()); transactionTemplate.executeWithoutResult(status -> { Menu result = adminMenuRepository.getById(1); @@ -949,107 +846,87 @@ void modifyManyOptionMenu() { } @Test - @DisplayName("어드민이 특정 상점의 메뉴를 여러옵션을 가진 메뉴로 수정한다. - 가격 옵션이 비어있거나 null이면 400 에러 발생") - void modifyManyOptionMenuWithEmptyOptionPrices() { + void 어드민이_특정_상점의_메뉴를_여러옵션을_가진_메뉴로_수정한다_가격_옵션이_비어있거나_null이면_400_에러_발생() throws Exception { // given Menu menu = menuFixture.짜장면_옵션메뉴(shop_마슬랜, menuCategory_메인); - RestAssured - .given() - .header("Authorization", "Bearer " + token_admin) - .pathParam("shopId", shop_마슬랜.getId()) - .pathParam("menuId", menu.getId()) - .contentType(ContentType.JSON) - .body(String.format(""" - { - "category_ids": [ - %d, %d - ], - "description": "테스트메뉴입니다.", - "image_urls": [ - "https://fixed-testimage.com/수정된짜장면.png" - ], - "is_single": false, - "name": "짜장면", - "option_prices": [] - } - """, menuCategory_메인.getId(), menuCategory_사이드.getId())) - .when() - .put("/admin/shops/{shopId}/menus/{menuId}") - .then() - .statusCode(HttpStatus.BAD_REQUEST.value()) - .extract(); + mockMvc.perform( + put("/admin/shops/{shopId}/menus/{menuId}", shop_마슬랜.getId(), menu.getId()) + .header("Authorization", "Bearer " + token_admin) + .contentType(MediaType.APPLICATION_JSON) + .content(String.format(""" + { + "category_ids": [ + %d, %d + ], + "description": "테스트메뉴입니다.", + "image_urls": [ + "https://fixed-testimage.com/수정된짜장면.png" + ], + "is_single": false, + "name": "짜장면", + "option_prices": [] + } + """, menuCategory_메인.getId(), menuCategory_사이드.getId())) + ) + .andExpect(status().isBadRequest()); } @Test - @DisplayName("어드민이 상점을 삭제한다.") - void deleteShop() { + void 어드민이_상점을_삭제한다() throws Exception { Shop shop = shopFixture.영업중이_아닌_신전_떡볶이(owner_현수); - RestAssured - .given() - .header("Authorization", "Bearer " + token_admin) - .when() - .delete("/admin/shops/{id}", shop.getId()) - .then() - .statusCode(HttpStatus.OK.value()); + mockMvc.perform( + delete("/admin/shops/{id}", shop.getId()) + .header("Authorization", "Bearer " + token_admin) + ) + .andExpect(status().isOk()); Shop deletedShop = adminShopRepository.getById(shop.getId()); assertSoftly(softly -> softly.assertThat(deletedShop.isDeleted()).isTrue()); } @Test - @DisplayName("어드민이 상점 카테고리를 삭제한다.") - void deleteShopCategory() { + void 어드민이_상점_카테고리를_삭제한다() throws Exception { ShopCategory shopCategory = shopCategoryFixture.카테고리_일반음식(); - RestAssured - .given() - .header("Authorization", "Bearer " + token_admin) - .when() - .delete("/admin/shops/categories/{id}", shopCategory.getId()) - .then() - .statusCode(HttpStatus.OK.value()); + mockMvc.perform( + delete("/admin/shops/categories/{id}", shopCategory.getId()) + .header("Authorization", "Bearer " + token_admin) + ) + .andExpect(status().isOk()); ShopCategory deletedCategory = adminShopCategoryRepository.getById(shopCategory.getId()); assertSoftly(softly -> softly.assertThat(deletedCategory.isDeleted()).isTrue()); } @Test - @DisplayName("어드민이 특정 상점의 메뉴 카테고리를 삭제한다.") - void deleteMenuCategory() { + void 어드민이_특정_상점의_메뉴_카테고리를_삭제한다() throws Exception { // when & then - RestAssured - .given() - .header("Authorization", "Bearer " + token_admin) - .pathParam("shopId", shop_마슬랜.getId()) - .pathParam("categoryId", menuCategory_메인.getId()) - .when() - .delete("/admin/shops/{shopId}/menus/categories/{categoryId}") - .then() - .statusCode(HttpStatus.NO_CONTENT.value()) - .extract(); + mockMvc.perform( + delete( + "/admin/shops/{shopId}/menus/categories/{categoryId}", + shop_마슬랜.getId(), + menuCategory_메인.getId() + ) + .header("Authorization", "Bearer " + token_admin) + ) + .andExpect(status().isNoContent()); assertThat(adminMenuCategoryRepository.findById(menuCategory_메인.getId())).isNotPresent(); } @Test - @DisplayName("어드민이 메뉴를 삭제한다.") - void deleteMenu() { + void 어드민이_메뉴를_삭제한다() throws Exception { // given Menu menu = menuFixture.짜장면_단일메뉴(shop_마슬랜, menuCategory_메인); - RestAssured - .given() - .header("Authorization", "Bearer " + token_admin) - .pathParam("shopId", shop_마슬랜.getId()) - .pathParam("menuId", menu.getId()) - .when() - .delete("/admin/shops/{shopId}/menus/{menuId}", menu.getId()) - .then() - .statusCode(HttpStatus.NO_CONTENT.value()) - .extract(); + mockMvc.perform( + delete("/admin/shops/{shopId}/menus/{menuId}", shop_마슬랜.getId(), menu.getId()) + .header("Authorization", "Bearer " + token_admin) + ) + .andExpect(status().isNoContent()); assertThat(adminMenuRepository.findById(menu.getId())).isNotPresent(); } diff --git a/src/test/java/in/koreatech/koin/admin/acceptance/AdminTrackApiTest.java b/src/test/java/in/koreatech/koin/admin/acceptance/AdminTrackApiTest.java index c9ea7ecc9..1f1c7fdb2 100644 --- a/src/test/java/in/koreatech/koin/admin/acceptance/AdminTrackApiTest.java +++ b/src/test/java/in/koreatech/koin/admin/acceptance/AdminTrackApiTest.java @@ -1,10 +1,15 @@ package in.koreatech.koin.admin.acceptance; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + import org.assertj.core.api.SoftAssertions; -import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.transaction.annotation.Transactional; import in.koreatech.koin.AcceptanceTest; import in.koreatech.koin.admin.member.repository.AdminTechStackRepository; @@ -17,10 +22,10 @@ import in.koreatech.koin.fixture.TechStackFixture; import in.koreatech.koin.fixture.TrackFixture; import in.koreatech.koin.fixture.UserFixture; -import in.koreatech.koin.support.JsonAssertions; -import io.restassured.RestAssured; @SuppressWarnings("NonAsciiCharacters") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Transactional public class AdminTrackApiTest extends AcceptanceTest { @Autowired @@ -41,25 +46,25 @@ public class AdminTrackApiTest extends AcceptanceTest { @Autowired private AdminTechStackRepository adminTechStackRepository; + @BeforeAll + void setup() { + clear(); + } + @Test - @DisplayName("관리자가 BCSDLab 트랙 정보를 조회한다. - 관리자가 아니면 403 반환") - void findTracksAdminNoAuth() { + void 관리자가_BCSDLab_트랙_정보를_조회한다_관리자가_아니면_403_반환() throws Exception { Student student = userFixture.준호_학생(); String token = userFixture.getToken(student.getUser()); - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .get("/admin/tracks") - .then() - .statusCode(HttpStatus.FORBIDDEN.value()) - .extract(); + mockMvc.perform( + get("/admin/tracks") + .header("Authorization", "Bearer " + token) + ) + .andExpect(status().isForbidden()); } @Test - @DisplayName("관리자가 BCSDLab 트랙 정보를 조회한다.") - void findTracks() { + void 관리자가_BCSDLab_트랙_정보를_조회한다() throws Exception { User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); @@ -67,17 +72,12 @@ void findTracks() { trackFixture.frontend(); trackFixture.ios(); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .get("/admin/tracks") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/admin/tracks") + .header("Authorization", "Bearer " + token) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" [ { "id": 1, @@ -104,33 +104,27 @@ void findTracks() { "updated_at": "2024-01-15 12:00:00" } ] - """); + """)); } @Test - @DisplayName("관리자가 BCSDLab 트랙 정보를 생성한다.") - void createTrack() { + void 관리자가_BCSDLab_트랙_정보를_생성한다() throws Exception { User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType("application/json") - .body(""" - { - "name": "BackEnd", - "headcount": 20 - } - """) - .when() - .post("/admin/tracks") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + post("/admin/tracks") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "name": "BackEnd", + "headcount": 20 + } + """) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "id": 1, "name": "BackEnd", @@ -139,37 +133,32 @@ void createTrack() { "created_at": "2024-01-15 12:00:00", "updated_at": "2024-01-15 12:00:00" } - """); + """)); } @Test - @DisplayName("관리자가 BCSDLab 트랙 정보를 생성한다. - 이미 있는 트랙명이면 409반환") - void createTrackDuplication() { + void 관리자가_BCSDLab_트랙_정보를_생성한다_이미_있는_트랙명이면_409_반환() throws Exception { User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); trackFixture.backend(); - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType("application/json") - .body(""" - { - "name": "BackEnd", - "headcount": 20 - } - """) - .when() - .post("/admin/tracks") - .then() - .statusCode(HttpStatus.CONFLICT.value()) - .extract(); + mockMvc.perform( + post("/admin/tracks") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "name": "BackEnd", + "headcount": 20 + } + """) + ) + .andExpect(status().isConflict()); } @Test - @DisplayName("관리자가 BCSDLab 트랙 단건 정보를 조회한다.") - void findTrack() { + void 관리자가_BCSDLab_트랙_단거_정보를_조회한다() throws Exception { User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); @@ -180,17 +169,12 @@ void findTrack() { techStackFixture.java(backend); techStackFixture.adobeFlash(backend); //삭제된 기술스택 - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .get("/admin/tracks/{id}", backend.getId()) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/admin/tracks/{id}", backend.getId()) + .header("Authorization", "Bearer " + token) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "TrackName": "BackEnd", "Members": [ @@ -242,35 +226,29 @@ void findTrack() { } ] } - """); + """)); } @Test - @DisplayName("관리자가 BCSDLab 트랙 정보를 수정한다.") - void updateTrack() { + void 관리자가_BCSDLab_트랙_정보를_수정한다() throws Exception { User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); Track backEnd = trackFixture.backend(); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType("application/json") - .body(""" - { - "name": "frontEnd", - "headcount": 20 - } - """) - .when() - .put("/admin/tracks/{id}", backEnd.getId()) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + put("/admin/tracks/{id}", backEnd.getId()) + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "name": "frontEnd", + "headcount": 20 + } + """) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "id": 1, "name": "frontEnd", @@ -279,50 +257,42 @@ void updateTrack() { "created_at": "2024-01-15 12:00:00", "updated_at": "2024-01-15 12:00:00" } - """); + """)); } @Test - @DisplayName("관리자가 BCSDLab 트랙 정보를 수정한다. - 이미 있는 트랙명이면 409반환") - void updateTrackDuplication() { + void 관리자가_BCSDLab_트랙_정보를_수정한다_이미_있는_트랙명이면_409_반환() throws Exception { User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); Track backEnd = trackFixture.backend(); - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType("application/json") - .body(""" - { - "name": "BackEnd", - "headcount": 20 - } - """) - .when() - .put("/admin/tracks/{id}", backEnd.getId()) - .then() - .statusCode(HttpStatus.CONFLICT.value()) - .extract(); + mockMvc.perform( + put("/admin/tracks/{id}", backEnd.getId()) + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "name": "BackEnd", + "headcount": 20 + } + """) + ) + .andExpect(status().isConflict()); } @Test - @DisplayName("관리자가 BCSDLab 트랙 정보를 삭제한다.") - void deleteTrack() { + void 관리자가_BCSDLab_트랙_정보를_삭제한다() throws Exception { User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); Track backEnd = trackFixture.backend(); - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .delete("/admin/tracks/{id}", backEnd.getId()) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + mockMvc.perform( + delete("/admin/tracks/{id}", backEnd.getId()) + .header("Authorization", "Bearer " + token) + ) + .andExpect(status().isOk()); Track updatedTrack = adminTrackRepository.getById(backEnd.getId()); @@ -334,34 +304,28 @@ void deleteTrack() { } @Test - @DisplayName("관리자가 BCSDLab 기술스택 정보를 생성한다.") - void createTechStack() { + void 관리자가_BCSDLab_기술스택_정보를_생성한다() throws Exception { User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); trackFixture.frontend(); Track backEnd = trackFixture.backend(); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType("application/json") - .body(""" - { - "image_url": "https://url.com", - "name": "Spring", - "description": "스프링은 웹 프레임워크이다" - } - """) - .when() - .queryParam("trackName", backEnd.getName()) - .post("/admin/techStacks") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + post("/admin/techStacks") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "image_url": "https://url.com", + "name": "Spring", + "description": "스프링은 웹 프레임워크이다" + } + """) + .param("trackName", backEnd.getName()) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "id": 1, "image_url": "https://url.com", @@ -372,39 +336,33 @@ void createTechStack() { "created_at": "2024-01-15 12:00:00", "updated_at": "2024-01-15 12:00:00" } - """); + """)); } @Test - @DisplayName("관리자가 BCSDLab 기술스택 정보를 수정한다.") - void updateTechStack() { + void 관리자가_BCSDLab_기술스택_정보를_수정한다() throws Exception { User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); TechStack java = techStackFixture.java(trackFixture.frontend()); Track backEnd = trackFixture.backend(); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType("application/json") - .body(""" - { - "image_url": "https://java.com", - "name": "JAVA", - "description": "java의 TrackID를 BackEnd로 수정한다.", - "is_deleted": true - } - """) - .when() - .queryParam("trackName", backEnd.getName()) - .put("/admin/techStacks/{id}", java.getId()) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + put("/admin/techStacks/{id}", java.getId()) + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "image_url": "https://java.com", + "name": "JAVA", + "description": "java의 TrackID를 BackEnd로 수정한다.", + "is_deleted": true + } + """) + .param("trackName", backEnd.getName()) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "id": 1, "image_url": "https://java.com", @@ -415,26 +373,23 @@ void updateTechStack() { "created_at": "2024-01-15 12:00:00", "updated_at": "2024-01-15 12:00:00" } - """); + """)); } @Test - @DisplayName("관리자가 기술스택 정보를 삭제한다.") - void deleteTechStack() { + void 관리자가_기술스택_정보를_삭제한다() throws Exception { User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); Track backEnd = trackFixture.backend(); TechStack java = techStackFixture.java(backEnd); - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .delete("/admin/techStacks/{id}", java.getId()) - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + mockMvc.perform( + delete("/admin/techStacks/{id}", java.getId()) + .header("Authorization", "Bearer " + token) + + ) + .andExpect(status().isOk()); TechStack updatedtechStack = adminTechStackRepository.getById(java.getId()); diff --git a/src/test/java/in/koreatech/koin/admin/acceptance/AdminUserApiTest.java b/src/test/java/in/koreatech/koin/admin/acceptance/AdminUserApiTest.java index 1c889fa92..152312a09 100644 --- a/src/test/java/in/koreatech/koin/admin/acceptance/AdminUserApiTest.java +++ b/src/test/java/in/koreatech/koin/admin/acceptance/AdminUserApiTest.java @@ -6,17 +6,24 @@ import static org.assertj.core.api.Assertions.assertThat; import static in.koreatech.koin.domain.user.model.UserType.STUDENT; import static org.assertj.core.api.SoftAssertions.assertSoftly; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; import java.util.ArrayList; import org.assertj.core.api.SoftAssertions; -import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; +import com.fasterxml.jackson.databind.JsonNode; + import in.koreatech.koin.AcceptanceTest; import in.koreatech.koin.admin.user.repository.AdminOwnerRepository; import in.koreatech.koin.admin.user.repository.AdminOwnerShopRedisRepository; @@ -33,10 +40,10 @@ import in.koreatech.koin.fixture.ShopFixture; import in.koreatech.koin.fixture.UserFixture; import in.koreatech.koin.support.JsonAssertions; -import io.restassured.RestAssured; -import io.restassured.http.ContentType; @SuppressWarnings("NonAsciiCharacters") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Transactional public class AdminUserApiTest extends AcceptanceTest { @Autowired @@ -66,26 +73,25 @@ public class AdminUserApiTest extends AcceptanceTest { @Autowired private PasswordEncoder passwordEncoder; + @BeforeAll + void setup() { + clear(); + } + @Test - @DisplayName("관리자가 학생 리스트를 파라미터가 없이 조회한다.(페이지네이션)") - void getStudentsWithoutParameterAdmin() { + void 관리자가_학생_리스트를_파라미터가_없이_조회한다_페이지네이션() throws Exception { Student student = userFixture.준호_학생(); User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType(ContentType.JSON) - .when() - .get("/admin/students") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/admin/students") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "current_count": 1, "current_page": 1, @@ -102,12 +108,11 @@ void getStudentsWithoutParameterAdmin() { "total_count": 1, "total_page": 1 } - """); + """)); } @Test - @DisplayName("관리자가 학생 리스트를 페이지 수와 limit으로 조회한다.(페이지네이션)") - void getStudentsWithPageAndLimitAdmin() { + void 관리자가_학생_리스트를_페이지_수와_limits으로_조회한다_페이지네이션() throws Exception { for (int i = 0; i < 11; i++) { Student student = Student.builder() .studentNumber("2019136135") @@ -137,20 +142,14 @@ void getStudentsWithPageAndLimitAdmin() { String token = userFixture.getToken(adminUser); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType(ContentType.JSON) - .queryParam("page", 2) - .queryParam("limit", 10) - .when() - .get("/admin/students") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/admin/students") + .header("Authorization", "Bearer " + token) + .param("page", "2") + .param("limit", "10") + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "current_count": 1, "current_page": 2, @@ -167,31 +166,25 @@ void getStudentsWithPageAndLimitAdmin() { "total_count": 11, "total_page": 2 } - """); + """)); } @Test - @DisplayName("관리자가 학생 리스트를 닉네임으로 조회한다.(페이지네이션)") - void getStudentsWithNicknameAdmin() { + void 관리자가_학생_리스트를_닉네임으로_조회한다_페이지네이션() throws Exception { Student student1 = userFixture.성빈_학생(); Student student2 = userFixture.준호_학생(); User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType(ContentType.JSON) - .when() - .queryParam("nickname", "준호") - .get("/admin/students") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/admin/students") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .param("nickname", "준호") + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "current_count": 1, "current_page": 1, @@ -208,120 +201,99 @@ void getStudentsWithNicknameAdmin() { "total_count": 1, "total_page": 1 } - """); + """)); } @Test - @DisplayName("관리자가 로그인 한다.") - void adminLogin() { + void 관리자가_로그인_한다() throws Exception { User adminUser = userFixture.코인_운영자(); String email = adminUser.getEmail(); String password = "1234"; - var response = RestAssured - .given() - .contentType(ContentType.JSON) - .body(""" - { - "email" : "%s", - "password" : "%s" - } - """.formatted(email, password)) - .when() - .post("/admin/user/login") - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); + mockMvc.perform( + post("/admin/user/login") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "email" : "%s", + "password" : "%s" + } + """.formatted(email, password)) + ) + .andExpect(status().isCreated()); } @Test - @DisplayName("관리자가 로그인 한다. - 관리자가 아니면 404 반환") - void adminLoginNoAuth() { + void 관리자가_로그인_한다_관리자가_아니면_404_반환() throws Exception { Student student = userFixture.준호_학생(); String email = student.getUser().getEmail(); String password = "1234"; - var response = RestAssured - .given() - .contentType(ContentType.JSON) - .body(""" - { - "email" : "%s", - "password" : "%s" - } - """.formatted(email, password)) - .when() - .post("/admin/user/login") - .then() - .statusCode(HttpStatus.NOT_FOUND.value()) - .extract(); + mockMvc.perform( + post("/admin/user/login") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "email" : "%s", + "password" : "%s" + } + """.formatted(email, password)) + ) + .andExpect(status().isNotFound()); } @Test - @DisplayName("관리자가 로그아웃한다") - void adminLogout() { + void 관리자가_로그아웃한다() throws Exception { User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType(ContentType.JSON) - .when() - .post("/admin/user/logout") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + mockMvc.perform( + post("/admin/user/logout") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()); } @Test - @DisplayName("관리자가 액세스 토큰 재발급 한다") - void adminRefresh() { + void 관리자가_액세스_토큰_재발급_한다() throws Exception { User adminUser = userFixture.코인_운영자(); String email = adminUser.getEmail(); String password = "1234"; String token = userFixture.getToken(adminUser); - var loginResponse = RestAssured - .given() - .contentType(ContentType.JSON) - .body(""" - { - "email" : "%s", - "password" : "%s" - } - """.formatted(email, password)) - .when() - .post("/admin/user/login") - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract() - .response(); - - String refreshToken = loginResponse.jsonPath().getString("refresh_token"); - - var refreshResponse = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType(ContentType.JSON) - .body(""" - { - "refresh_token" : "%s" - } - """.formatted(refreshToken)) - .when() - .post("/admin/user/refresh") - .then() - .statusCode(HttpStatus.CREATED.value()) - .extract(); + MvcResult result = mockMvc.perform( + post("/admin/user/login") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "email" : "%s", + "password" : "%s" + } + """.formatted(email, password)) + ) + .andExpect(status().isCreated()) + .andReturn(); + + JsonNode loginJsonNode = JsonAssertions.convertJsonNode(result); + + mockMvc.perform( + post("/admin/user/refresh") + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "refresh_token" : "%s" + } + """.formatted(loginJsonNode.get("refresh_token").asText())) + ) + .andExpect(status().isCreated()); } - @Test - @DisplayName("관리자가 사장님 권한 요청을 허용한다.") - void allowOwnerPermission() { + void 관리자가_사장님_권한_요청을_허용한다() throws Exception { Owner owner = userFixture.철수_사장님(); Shop shop = shopFixture.마슬랜(null); @@ -335,15 +307,11 @@ void allowOwnerPermission() { ownerShopRedisRepository.save(ownerShop); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .pathParam("id", owner.getUser().getId()) - .put("/admin/owner/{id}/authed") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + mockMvc.perform( + put("/admin/owner/{id}/authed", owner.getUser().getId()) + .header("Authorization", "Bearer " + token) + ) + .andExpect(status().isOk()); //영속성 컨테스트 동기화 Owner updatedOwner = adminOwnerRepository.getById(owner.getId()); @@ -359,44 +327,32 @@ void allowOwnerPermission() { } @Test - @DisplayName("관리자가 특정 학생 정보를 조회한다. - 관리자가 아니면 403 반환") - void studentUpdateAdminNoAuth() { + void 관리자가_특정_학생_정보를_조회한다_관리자가_아니면_403_반환() throws Exception { Student student = userFixture.준호_학생(); String token = userFixture.getToken(student.getUser()); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType(ContentType.JSON) - .when() - .pathParam("id", student.getUser().getId()) - .get("/admin/users/student/{id}") - .then() - .statusCode(HttpStatus.FORBIDDEN.value()) - .extract(); + mockMvc.perform( + get("/admin/users/student/{id}", student.getUser().getId()) + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isForbidden()); } @Test - @DisplayName("관리자가 특정 학생 정보를 조회한다.") - void studentGetAdmin() { + void 관리자가_특정_학생_정보를_조회한다() throws Exception { Student student = userFixture.준호_학생(); User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType(ContentType.JSON) - .when() - .pathParam("id", student.getUser().getId()) - .get("/admin/users/student/{id}") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + get("/admin/users/student/{id}", student.getUser().getId()) + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "anonymous_nickname": "익명", "created_at": "2024-01-15 12:00:00", @@ -414,38 +370,45 @@ void studentGetAdmin() { "updated_at": "2024-01-15 12:00:00", "user_type": "STUDENT" } - """); + """)); } @Test - @DisplayName("관리자가 특정 학생 정보를 수정한다.") - void studentUpdateAdmin() { + void 관리자가_특정_학생_정보를_수정한다() throws Exception { Student student = userFixture.준호_학생(); User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType(ContentType.JSON) - .body(""" - { - "gender" : 1, - "major" : "기계공학부", - "name" : "서정빈", - "password" : "0c4be6acaba1839d3433c1ccf04e1eec4d1fa841ee37cb019addc269e8bc1b77", - "nickname" : "duehee", - "phone_number" : "01023456789", - "student_number" : "2019136136" - } - """) - .when() - .pathParam("id", student.getUser().getId()) - .put("/admin/users/student/{id}") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + mockMvc.perform( + put("/admin/users/student/{id}", student.getUser().getId()) + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "gender" : 1, + "major" : "기계공학부", + "name" : "서정빈", + "password" : "0c4be6acaba1839d3433c1ccf04e1eec4d1fa841ee37cb019addc269e8bc1b77", + "nickname" : "duehee", + "phone_number" : "01023456789", + "student_number" : "2019136136" + } + """) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { + "anonymous_nickname": "익명", + "email": "juno@koreatech.ac.kr", + "gender": 1, + "major": "기계공학부", + "name": "서정빈", + "nickname": "duehee", + "phone_number": "01023456789", + "student_number": "2019136136" + } + """)); transactionTemplate.executeWithoutResult(status -> { Student result = adminStudentRepository.getById(student.getId()); @@ -458,43 +421,22 @@ void studentUpdateAdmin() { } ); }); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" - { - "anonymous_nickname": "익명", - "email": "juno@koreatech.ac.kr", - "gender": 1, - "major": "기계공학부", - "name": "서정빈", - "nickname": "duehee", - "phone_number": "01023456789", - "student_number": "2019136136" - } - """); } @Test - @DisplayName("관리자가 특정 사장을 조회한다.") - void getOwnerAdmin() { + void 관리자가_특정_사장을_조회한다() throws Exception { Owner owner = userFixture.현수_사장님(); Shop shop = shopFixture.마슬랜(owner); User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .pathParam("id", owner.getUser().getId()) - .get("/admin/users/owner/{id}") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + mockMvc.perform( + get("/admin/users/owner/{id}", owner.getUser().getId()) + .header("Authorization", "Bearer " + token) + ) + .andExpect(status().isOk()) + .andExpect(content().json(String.format(""" { "id": 1, "email": "hysoo@naver.com", @@ -516,39 +458,31 @@ void getOwnerAdmin() { "updated_at" : "2024-01-15 12:00:00", "last_logged_at" : null } - """, shop.getId() - )); + """, shop.getId()))); } @Test - @DisplayName("관리자가 특정 사장을 수정한다.") - void updateOwner() { + void 관리자가_특정_사장을_수정한다() throws Exception { Owner owner = userFixture.현수_사장님(); Shop shop = shopFixture.마슬랜(owner); User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType(ContentType.JSON) - .body(""" - { - "company_registration_number" : "123-45-67190", - "grant_shop" : "false", - "grant_event" : "false" - } - """) - .when() - .pathParam("id", owner.getUser().getId()) - .put("/admin/users/owner/{id}") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(""" + mockMvc.perform( + put("/admin/users/owner/{id}", owner.getUser().getId()) + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "company_registration_number" : "123-45-67190", + "grant_shop" : "false", + "grant_event" : "false" + } + """) + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "company_registration_number" : "123-45-67190", "email" : "hysoo@naver.com", @@ -559,12 +493,11 @@ void updateOwner() { "nickname" : "현수", "phone_number" : "01098765432" } - """); + """)); } @Test - @DisplayName("관리자가 가입 신청한 사장님 리스트 조회한다.") - void getNewOwnersAdmin() { + void 관리자가_가입_신청한_사장님_리스트_조회한다() throws Exception { Owner owner = userFixture.철수_사장님(); Shop shop = shopFixture.마슬랜(null); @@ -572,26 +505,21 @@ void getNewOwnersAdmin() { String token = userFixture.getToken(adminUser); OwnerShop ownerShop = OwnerShop.builder() - .ownerId(owner.getId()) - .shopId(shop.getId()) - .build(); + .ownerId(owner.getId()) + .shopId(shop.getId()) + .build(); ownerShopRedisRepository.save(ownerShop); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .param("searchType", "NAME") - .param("query", "철수") - .param("sort", "CREATED_AT_DESC") - .get("/admin/users/new-owners") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - JsonAssertions.assertThat(response.asPrettyString()) - .isEqualTo(String.format(""" + mockMvc.perform( + get("/admin/users/new-owners") + .header("Authorization", "Bearer " + token) + .param("searchType", "NAME") + .param("query", "철수") + .param("sort", "CREATED_AT_DESC") + ) + .andExpect(status().isOk()) + .andExpect(content().json(""" { "total_count": 1, "current_count": 1, @@ -609,14 +537,11 @@ void getNewOwnersAdmin() { } ] } - """ - )); + """)); } @Test - @DisplayName("관리자가 가입 신청한 사장님 리스트 조회한다 - V2") - void getNewOwnersAdminV2() { - + void 관리자가_가입_신청한_사장님_리스트_조회한다_V2() throws Exception { for (int i = 0; i < 11; i++) { User user = User.builder() .password(passwordEncoder.encode("1234")) @@ -659,29 +584,20 @@ void getNewOwnersAdminV2() { User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .get("/admin/users/new-owners") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - assertSoftly( - softly -> { - softly.assertThat(response.body().jsonPath().getInt("total_count")).isEqualTo(11); - softly.assertThat(response.body().jsonPath().getInt("current_count")).isEqualTo(10); - softly.assertThat(response.body().jsonPath().getInt("total_page")).isEqualTo(2); - softly.assertThat(response.body().jsonPath().getInt("current_page")).isEqualTo(1); - softly.assertThat(response.body().jsonPath().getList("owners").size()).isEqualTo(10); - } - ); + mockMvc.perform( + get("/admin/users/new-owners") + .header("Authorization", "Bearer " + token) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.total_count").value(11)) + .andExpect(jsonPath("$.current_count").value(10)) + .andExpect(jsonPath("$.total_page").value(2)) + .andExpect(jsonPath("$.current_page").value(1)) + .andExpect(jsonPath("$.owners.length()").value(10)); } @Test - @DisplayName("관리자가 가입 사장님 리스트 조회한다") - void getOwnersAdmin() { + void 관리자가_가입_사장님_리스트_조회한다() throws Exception { for (int i = 0; i < 11; i++) { User user = User.builder() .password(passwordEncoder.encode("1234")) @@ -724,72 +640,50 @@ void getOwnersAdmin() { User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .get("/admin/users/owners") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - assertSoftly( - softly -> { - softly.assertThat(response.body().jsonPath().getInt("total_count")).isEqualTo(11); - softly.assertThat(response.body().jsonPath().getInt("current_count")).isEqualTo(10); - softly.assertThat(response.body().jsonPath().getInt("total_page")).isEqualTo(2); - softly.assertThat(response.body().jsonPath().getInt("current_page")).isEqualTo(1); - softly.assertThat(response.body().jsonPath().getList("owners").size()).isEqualTo(10); - } - ); + mockMvc.perform( + get("/admin/users/owners") + .header("Authorization", "Bearer " + token) + + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.total_count").value(11)) + .andExpect(jsonPath("$.current_count").value(10)) + .andExpect(jsonPath("$.total_page").value(2)) + .andExpect(jsonPath("$.current_page").value(1)) + .andExpect(jsonPath("$.owners.length()").value(10)); } @Test - @DisplayName("관리자가 회원을 조회한다.") - void getUser() { + void 관리자가_회원을_조회한다() throws Exception { Student student = userFixture.준호_학생(); User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); - var response = RestAssured - .given() - .header("Authorization", "Bearer " + token) - .when() - .pathParam("id", student.getUser().getId()) - .get("/admin/users/{id}") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); - - assertSoftly( - softly -> { - softly.assertThat(response.body().jsonPath().getString("nickname")).isEqualTo("준호"); - softly.assertThat(response.body().jsonPath().getString("name")).isEqualTo("테스트용_준호"); - softly.assertThat(response.body().jsonPath().getString("phoneNumber")).isEqualTo("01012345678"); - softly.assertThat(response.body().jsonPath().getString("email")).isEqualTo("juno@koreatech.ac.kr"); - } - ); + mockMvc.perform( + get("/admin/users/{id}", student.getUser().getId()) + .header("Authorization", "Bearer " + token) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.nickname").value("준호")) + .andExpect(jsonPath("$.name").value("테스트용_준호")) + .andExpect(jsonPath("$.phoneNumber").value("01012345678")) + .andExpect(jsonPath("$.email").value("juno@koreatech.ac.kr")); } @Test - @DisplayName("관리자가 회원을 삭제한다.") - void deleteUser() { + void 관리자가_회원을_삭제한다() throws Exception { Student student = userFixture.준호_학생(); User adminUser = userFixture.코인_운영자(); String token = userFixture.getToken(adminUser); - RestAssured - .given() - .header("Authorization", "Bearer " + token) - .contentType(ContentType.JSON) - .when() - .pathParam("id", student.getUser().getId()) - .delete("/admin/users/{id}") - .then() - .statusCode(HttpStatus.OK.value()) - .extract(); + mockMvc.perform( + delete("/admin/users/{id}", student.getUser().getId()) + .header("Authorization", "Bearer " + token) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()); assertThat(adminUserRepository.findById(student.getId())).isNotPresent(); } diff --git a/src/test/java/in/koreatech/koin/fixture/AbtestFixture.java b/src/test/java/in/koreatech/koin/fixture/AbtestFixture.java new file mode 100644 index 000000000..877885938 --- /dev/null +++ b/src/test/java/in/koreatech/koin/fixture/AbtestFixture.java @@ -0,0 +1,100 @@ +package in.koreatech.koin.fixture; + +import java.util.List; + +import org.springframework.stereotype.Component; + +import in.koreatech.koin.admin.abtest.model.Abtest; +import in.koreatech.koin.admin.abtest.model.AbtestStatus; +import in.koreatech.koin.admin.abtest.model.AbtestVariable; +import in.koreatech.koin.admin.abtest.repository.AbtestRepository; + +@Component +public final class AbtestFixture { + + private final AbtestRepository abtestRepository; + + public AbtestFixture(AbtestRepository abtestRepository) { + this.abtestRepository = abtestRepository; + } + + public Abtest 식단_UI_실험() { + Abtest abtest = + Abtest.builder() + .title("dining_ui_test") + .displayTitle("식단_UI_실험") + .description("세부설명") + .creator("송선권") + .team("campus") + .status(AbtestStatus.IN_PROGRESS) + .build(); + + AbtestVariable abtestVariable = + AbtestVariable.builder() + .abtest(abtest) + .name("A") + .displayName("실험군 A") + .rate(50) + .count(0) + .build(); + + AbtestVariable abtestVariable2 = + AbtestVariable.builder() + .abtest(abtest) + .name("B") + .displayName("실험군 B") + .rate(50) + .count(0) + .build(); + + abtest.getAbtestVariables().addAll(List.of(abtestVariable, abtestVariable2)); + return abtestRepository.save(abtest); + } + + public Abtest 식단_UI_실험(int titleNumber) { + Abtest abtest = + Abtest.builder() + .title("dining_ui_test" + titleNumber) + .displayTitle("식단_UI_실험") + .description("세부설명") + .creator("송선권") + .team("campus") + .status(AbtestStatus.IN_PROGRESS) + .build(); + + AbtestVariable abtestVariable = + AbtestVariable.builder() + .abtest(abtest) + .name("A") + .displayName("실험군 A") + .rate(50) + .count(0) + .build(); + + AbtestVariable abtestVariable2 = + AbtestVariable.builder() + .abtest(abtest) + .name("B") + .displayName("실험군 B") + .rate(50) + .count(0) + .build(); + + abtest.getAbtestVariables().addAll(List.of(abtestVariable, abtestVariable2)); + return abtestRepository.save(abtest); + + } + + public Abtest 주변상점_UI_실험() { + return abtestRepository.save( + Abtest.builder() + .title("shop_ui_test") + .displayTitle("주변상점_UI_실험") + .description("세부설명") + .creator("송선권") + .team("campus") + .status(AbtestStatus.IN_PROGRESS) + .build() + ); + } +} diff --git a/src/test/java/in/koreatech/koin/fixture/DeviceFixture.java b/src/test/java/in/koreatech/koin/fixture/DeviceFixture.java new file mode 100644 index 000000000..be217dd9b --- /dev/null +++ b/src/test/java/in/koreatech/koin/fixture/DeviceFixture.java @@ -0,0 +1,47 @@ +package in.koreatech.koin.fixture; + +import org.springframework.stereotype.Component; + +import in.koreatech.koin.admin.abtest.model.AccessHistory; +import in.koreatech.koin.admin.abtest.model.Device; +import in.koreatech.koin.admin.abtest.repository.DeviceRepository; +import in.koreatech.koin.domain.user.repository.UserRepository; + +@Component +public class DeviceFixture { + + private final DeviceRepository deviceRepository; + private final UserRepository userRepository; + + public DeviceFixture( + DeviceRepository deviceRepository, + UserRepository userRepository + ) { + this.deviceRepository = deviceRepository; + this.userRepository = userRepository; + } + + public Device 아이폰(Integer userId) { + AccessHistory accessHistory = AccessHistory.builder().build(); + Device device = Device.builder() + .accessHistory(accessHistory) + .user(userRepository.getById(userId)) + .model("아이폰14") + .type("mobile") + .build(); + accessHistory.connectDevice(device); + return deviceRepository.save(device); + } + + public Device 갤럭시(Integer userId) { + AccessHistory accessHistory = AccessHistory.builder().build(); + Device device = Device.builder() + .accessHistory(accessHistory) + .user(userRepository.getById(userId)) + .model("갤럭시24") + .type("mobile") + .build(); + accessHistory.connectDevice(device); + return deviceRepository.save(device); + } +} diff --git a/src/test/java/in/koreatech/koin/fixture/MenuFixture.java b/src/test/java/in/koreatech/koin/fixture/MenuFixture.java index 3b89c474d..2921d45dd 100644 --- a/src/test/java/in/koreatech/koin/fixture/MenuFixture.java +++ b/src/test/java/in/koreatech/koin/fixture/MenuFixture.java @@ -29,13 +29,11 @@ public MenuFixture( } public Menu 짜장면_옵션메뉴(Shop shop, MenuCategory menuCategory) { - Menu menu = menuRepository.save( - Menu.builder() - .shopId(shop.getId()) - .name("짜장면") - .description("맛있는 짜장면") - .build() - ); + Menu menu = Menu.builder() + .shopId(shop.getId()) + .name("짜장면") + .description("맛있는 짜장면") + .build(); menu.getMenuImages().addAll( List.of( @@ -63,25 +61,21 @@ public MenuFixture( .build() ) ); - menu.getMenuCategoryMaps().add( - MenuCategoryMap.builder() - .menu(menu) - .menuCategory(menuCategory) - .build() - ); - + MenuCategoryMap menuCategoryMap = MenuCategoryMap.builder() + .menu(menu) + .menuCategory(menuCategory) + .build(); + menu.getMenuCategoryMaps().add(menuCategoryMap); + menuCategory.getMenuCategoryMaps().add(menuCategoryMap); return menuRepository.save(menu); } public Menu 짜장면_단일메뉴(Shop shop, MenuCategory menuCategory) { - - Menu menu = menuRepository.save( - Menu.builder() - .shopId(shop.getId()) - .name("짜장면") - .description("맛있는 짜장면") - .build() - ); + Menu menu = Menu.builder() + .shopId(shop.getId()) + .name("짜장면") + .description("맛있는 짜장면") + .build(); menu.getMenuImages().addAll( List.of( @@ -102,13 +96,12 @@ public MenuFixture( .price(7000) .build() ); - menu.getMenuCategoryMaps().add( - MenuCategoryMap.builder() - .menu(menu) - .menuCategory(menuCategory) - .build() - ); - + MenuCategoryMap menuCategoryMap = MenuCategoryMap.builder() + .menu(menu) + .menuCategory(menuCategory) + .build(); + menu.getMenuCategoryMaps().add(menuCategoryMap); + menuCategory.getMenuCategoryMaps().add(menuCategoryMap); return menuRepository.save(menu); } } diff --git a/src/test/java/in/koreatech/koin/fixture/ShopReviewFixture.java b/src/test/java/in/koreatech/koin/fixture/ShopReviewFixture.java index 0e8cb40e4..0c26cd572 100644 --- a/src/test/java/in/koreatech/koin/fixture/ShopReviewFixture.java +++ b/src/test/java/in/koreatech/koin/fixture/ShopReviewFixture.java @@ -9,6 +9,7 @@ import in.koreatech.koin.domain.shop.model.ShopReview; import in.koreatech.koin.domain.shop.model.ShopReviewImage; import in.koreatech.koin.domain.shop.model.ShopReviewMenu; +import in.koreatech.koin.domain.shop.repository.ShopRepository; import in.koreatech.koin.domain.shop.repository.ShopReviewImageRepository; import in.koreatech.koin.domain.shop.repository.ShopReviewMenuRepository; import in.koreatech.koin.domain.shop.repository.ShopReviewRepository; @@ -27,8 +28,8 @@ public ShopReviewFixture( ShopReviewRepository shopReviewRepository, ShopReviewImageRepository shopReviewImageRepository, ShopReviewMenuRepository shopReviewMenuRepository, - Clock clock - ) { + Clock clock, + ShopRepository shopRepository) { this.shopReviewRepository = shopReviewRepository; this.shopReviewImageRepository = shopReviewImageRepository; this.shopReviewMenuRepository = shopReviewMenuRepository; @@ -51,8 +52,9 @@ public ShopReviewFixture( .menuName("피자") .review(shopReview) .build()); - ShopReview savedShopReview = shopReviewRepository.save(shopReview); - return savedShopReview; + shopReviewRepository.save(shopReview); + shop.getReviews().add(shopReview); + return shopReview; } @FixedDate(year = 2024, month = 8, day = 7) diff --git a/src/test/java/in/koreatech/koin/fixture/UserFixture.java b/src/test/java/in/koreatech/koin/fixture/UserFixture.java index 3ccddd0b9..ff575d3a3 100644 --- a/src/test/java/in/koreatech/koin/fixture/UserFixture.java +++ b/src/test/java/in/koreatech/koin/fixture/UserFixture.java @@ -2,10 +2,7 @@ import static in.koreatech.koin.domain.user.model.UserGender.MAN; import static in.koreatech.koin.domain.user.model.UserIdentity.UNDERGRADUATE; -import static in.koreatech.koin.domain.user.model.UserType.ADMIN; -import static in.koreatech.koin.domain.user.model.UserType.COOP; -import static in.koreatech.koin.domain.user.model.UserType.OWNER; -import static in.koreatech.koin.domain.user.model.UserType.STUDENT; +import static in.koreatech.koin.domain.user.model.UserType.*; import java.time.LocalDateTime; import java.util.ArrayList; @@ -132,8 +129,8 @@ public UserFixture( .user( User.builder() .password(passwordEncoder.encode("1234")) - .nickname("성빈") - .name("테스트용_성빈") + .nickname("빈") + .name("박성빈") .phoneNumber("01099411123") .userType(STUDENT) .gender(MAN) @@ -146,6 +143,46 @@ public UserFixture( ); } + public Owner 성빈_사장님() { + User user = User.builder() + .password(passwordEncoder.encode("1234")) + .nickname("성빈") + .name("박성빈") + .phoneNumber("01098765439") + .userType(OWNER) + .gender(MAN) + .email("testsungbeenowner@naver.com") + .isAuthed(true) + .isDeleted(false) + .build(); + + Owner owner = Owner.builder() + .account("01098765439") + .user(user) + .companyRegistrationNumber("723-45-67190") + .grantShop(true) + .grantEvent(true) + .attachments(new ArrayList<>()) + .build(); + + OwnerAttachment attachment1 = OwnerAttachment.builder() + .url("https://test.com/성빈_사장님_인증사진_8.jpg") + .isDeleted(false) + .owner(owner) + .build(); + + OwnerAttachment attachment2 = OwnerAttachment.builder() + .url("https://test.com/성빈_사장님_인증사진_9.jpg") + .isDeleted(false) + .owner(owner) + .build(); + + owner.getAttachments().add(attachment1); + owner.getAttachments().add(attachment2); + + return ownerRepository.save(owner); + } + public Owner 현수_사장님() { User user = User.builder() .password(passwordEncoder.encode("1234")) @@ -329,6 +366,15 @@ public UserFixture( return coopRepository.save(coop); } + public String 맥북userAgent헤더() { + return "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/123.45 (KHTML, like Gecko) Chrome/127.0.0" + + ".0 Safari/123.45, sec-fetch-dest=empty}"; + } + + public String 아이피() { + return "127.0.0.1"; + } + public String getToken(User user) { return jwtProvider.createToken(user); } @@ -352,7 +398,6 @@ public final class UserFixtureBuilder { private Boolean isDeleted; private String resetToken; private LocalDateTime resetExpiredAt; - private String deviceToken; public UserFixtureBuilder password(String password) { this.password = passwordEncoder.encode(password); @@ -419,16 +464,10 @@ public UserFixtureBuilder resetExpiredAt(LocalDateTime resetExpiredAt) { return this; } - public UserFixtureBuilder deviceToken(String deviceToken) { - this.deviceToken = deviceToken; - return this; - } - public User build() { return userRepository.save( User.builder() .phoneNumber(phoneNumber) - .deviceToken(deviceToken) .lastLoggedAt(lastLoggedAt) .isAuthed(isAuthed) .resetExpiredAt(resetExpiredAt) diff --git a/src/test/java/in/koreatech/koin/support/DBInitializer.java b/src/test/java/in/koreatech/koin/support/DBInitializer.java index fc115ae07..26a82152d 100644 --- a/src/test/java/in/koreatech/koin/support/DBInitializer.java +++ b/src/test/java/in/koreatech/koin/support/DBInitializer.java @@ -1,7 +1,5 @@ package in.koreatech.koin.support; -import java.sql.ResultSet; -import java.sql.Statement; import java.util.ArrayList; import java.util.List; @@ -24,7 +22,7 @@ public class DBInitializer { private static final int ON = 1; private static final int COLUMN_INDEX = 1; - private final List tableNames = new ArrayList<>(); + private List tableNames = new ArrayList<>(); @Autowired private DataSource dataSource; @@ -39,24 +37,27 @@ public class DBInitializer { private MongoTemplate mongoTemplate; private void findDatabaseTableNames() { - try (final Statement statement = dataSource.getConnection().createStatement()) { - ResultSet resultSet = statement.executeQuery("SHOW TABLES"); - while (resultSet.next()) { - final String tableName = resultSet.getString(COLUMN_INDEX); - tableNames.add(tableName); - } - } catch (Exception ignore) { - } + String sql = "SHOW TABLES"; + tableNames = entityManager.createNativeQuery(sql).getResultList(); } - private void truncate() { + private void truncateAllTable() { setForeignKeyCheck(OFF); - for (String tableName : tableNames) { + for (String tableName: tableNames) { entityManager.createNativeQuery(String.format("TRUNCATE TABLE %s", tableName)).executeUpdate(); } setForeignKeyCheck(ON); } + @Transactional + public void initIncrement() { + String sql = "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'test' AND AUTO_INCREMENT >= 1"; + List dirtyTables = entityManager.createNativeQuery(sql).getResultList(); + for (String tableName: dirtyTables) { + entityManager.createNativeQuery(String.format("ALTER TABLE %s AUTO_INCREMENT = 1", tableName)).executeUpdate(); + } + } + private void setForeignKeyCheck(int mode) { entityManager.createNativeQuery(String.format("SET FOREIGN_KEY_CHECKS = %d", mode)).executeUpdate(); } @@ -67,12 +68,16 @@ public void clear() { findDatabaseTableNames(); } entityManager.clear(); - truncate(); - // Redis 초기화 + truncateAllTable(); + clearRedis(); + clearMongo(); + } + + public void clearRedis() { redisTemplate.getConnectionFactory().getConnection().flushAll(); - // Mongo 초기화 - for (String collectionName : mongoTemplate.getCollectionNames()) { - mongoTemplate.remove(new Query(), collectionName); - } + } + + private void clearMongo() { + mongoTemplate.getCollectionNames().forEach(collectionName -> mongoTemplate.remove(new Query(), collectionName)); } } diff --git a/src/test/java/in/koreatech/koin/support/JsonAssertions.java b/src/test/java/in/koreatech/koin/support/JsonAssertions.java index c7ef8b5d7..9a53b47b6 100644 --- a/src/test/java/in/koreatech/koin/support/JsonAssertions.java +++ b/src/test/java/in/koreatech/koin/support/JsonAssertions.java @@ -1,22 +1,56 @@ package in.koreatech.koin.support; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.util.List; import java.util.Map; import org.assertj.core.api.Assertions; +import org.hibernate.query.sqm.sql.ConversionException; +import org.springframework.test.web.servlet.MvcResult; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import in.koreatech.koin.domain.bus.dto.SingleBusTimeResponse; public class JsonAssertions { private static final ObjectMapper objectMapper = new ObjectMapper(); + static { + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.findAndRegisterModules(); // 다른 모듈들도 자동 등록 + } + public static JsonStringAssert assertThat(String expect) { return new JsonStringAssert(expect); } + public static JsonNode convertJsonNode(MvcResult mvcResult) { + try { + return objectMapper.readTree(mvcResult.getResponse().getContentAsString()); + } catch (JsonProcessingException e) { + throw new ConversionException("JsonString to JsonNode convert exception: " + e.getMessage()); + } catch (UnsupportedEncodingException e) { + throw new ConversionException("Response to String convert exception: " + e.getMessage()); + } + } + + public static List convertToList(JsonNode jsonNode, Class clazz) { + try { + return objectMapper.readValue( + jsonNode.toString(), + objectMapper.getTypeFactory().constructCollectionType(List.class, clazz) + ); + } catch (JsonProcessingException e) { + throw new ConversionException("JsonNode to List conversion exception: " + e.getMessage()); + } + } + public static class JsonStringAssert { private final String expect; diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 0d9e7584f..a1732f66a 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -30,11 +30,21 @@ spring: server: tomcat: max-http-form-post-size: 10MB +# MockMvc이용하면 한글 깨지는 문제 해결하는 설정 + servlet: + encoding: + force-response: true logging: level: org: + springframework: + transaction: DEBUG # 스프링 트랜잭션 관련 로그 출력 + orm.jpa.JpaTransactionManager: DEBUG hibernate: + engine: + transaction: + internal: DEBUG # Hibernate 트랜잭션 로그 상세 출력 type: descriptor: sql: trace @@ -45,6 +55,7 @@ swagger: slack: koin_event_notify_url: https://slack-weehookurl.com koin_owner_event_notify_url: https://slack-weehookurl.com + koin_shop_review_notify_url: https://slack-weehookurl.com logging: error: https://slack-weehookurl.com