diff --git a/docs/open-api.yaml b/docs/open-api.yaml index 1fd1c323..789a05e7 100644 --- a/docs/open-api.yaml +++ b/docs/open-api.yaml @@ -64,6 +64,10 @@ paths: responses: "200": description: OK + content: + '*/*': + schema: + type: object /users/signup: post: tags: @@ -97,6 +101,23 @@ paths: "200": description: OK /reservations: + get: + tags: + - reservation-controller + operationId: getReservations + parameters: + - name: pageable + in: query + required: true + schema: + $ref: '#/components/schemas/Pageable' + responses: + "200": + description: OK + content: + '*/*': + schema: + $ref: '#/components/schemas/PageReservation' post: tags: - reservation-controller @@ -115,6 +136,28 @@ paths: schema: $ref: '#/components/schemas/ReservationResponse' /events: + get: + tags: + - event-controller + operationId: getEvents + parameters: + - name: title + in: query + required: false + schema: + type: string + - name: pageable + in: query + required: true + schema: + $ref: '#/components/schemas/Pageable' + responses: + "200": + description: OK + content: + '*/*': + schema: + $ref: '#/components/schemas/PageEvent' post: tags: - event-controller @@ -137,13 +180,19 @@ paths: tags: - bookmark-controller operationId: getBookmarks + parameters: + - name: pageable + in: query + required: true + schema: + $ref: '#/components/schemas/Pageable' responses: "200": description: OK content: '*/*': schema: - type: object + $ref: '#/components/schemas/PageBookmark' post: tags: - bookmark-controller @@ -218,20 +267,6 @@ paths: '*/*': schema: $ref: '#/components/schemas/EventResponse' - /events/: - get: - tags: - - event-controller - operationId: getEvents - responses: - "200": - description: OK - content: - '*/*': - schema: - type: array - items: - $ref: '#/components/schemas/EventResponse' /bookmarks/{id}: get: tags: @@ -416,3 +451,289 @@ components: event_id: type: integer format: int32 + Pageable: + type: object + properties: + page: + minimum: 0 + type: integer + format: int32 + size: + minimum: 1 + type: integer + format: int32 + sort: + type: array + items: + type: string + Bookmark: + required: + - createdAt + - event + - updatedAt + - user + type: object + properties: + id: + type: integer + format: int32 + user: + $ref: '#/components/schemas/User' + event: + $ref: '#/components/schemas/Event' + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + Event: + required: + - bookmarks + - createdAt + - currentReservationCount + - date + - maxAttendees + - reservationEndTime + - reservationStartTime + - reservations + - title + - updatedAt + type: object + properties: + id: + type: integer + format: int32 + title: + type: string + date: + type: string + format: date-time + reservationStartTime: + type: string + format: date-time + reservationEndTime: + type: string + format: date-time + maxAttendees: + type: integer + format: int32 + currentReservationCount: + type: integer + format: int32 + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + bookmarks: + type: array + items: + $ref: '#/components/schemas/Bookmark' + reservations: + type: array + items: + $ref: '#/components/schemas/Reservation' + GrantedAuthority: + type: object + properties: + authority: + type: string + PageReservation: + type: object + properties: + totalElements: + type: integer + format: int64 + totalPages: + type: integer + format: int32 + size: + type: integer + format: int32 + content: + type: array + items: + $ref: '#/components/schemas/Reservation' + number: + type: integer + format: int32 + sort: + $ref: '#/components/schemas/SortObject' + first: + type: boolean + last: + type: boolean + numberOfElements: + type: integer + format: int32 + pageable: + $ref: '#/components/schemas/PageableObject' + empty: + type: boolean + PageableObject: + type: object + properties: + offset: + type: integer + format: int64 + sort: + $ref: '#/components/schemas/SortObject' + pageNumber: + type: integer + format: int32 + pageSize: + type: integer + format: int32 + paged: + type: boolean + unpaged: + type: boolean + Reservation: + required: + - bookedAt + - event + - user + type: object + properties: + id: + type: integer + format: int32 + user: + $ref: '#/components/schemas/User' + event: + $ref: '#/components/schemas/Event' + bookedAt: + type: string + format: date-time + SortObject: + type: object + properties: + empty: + type: boolean + sorted: + type: boolean + unsorted: + type: boolean + User: + required: + - authorities + - createdAt + - email + - isAccountNonExpired + - isAccountNonLocked + - isCredentialsNonExpired + - isEnabled + - name + - password + - pw + - role + - updatedAt + - username + type: object + properties: + name: + type: string + email: + type: string + password: + type: string + authority: + type: string + writeOnly: true + enum: + - USER + id: + type: integer + format: int32 + createdAt: + type: string + format: date-time + updatedAt: + type: string + format: date-time + pw: + type: string + role: + type: string + enum: + - USER + isEnabled: + type: boolean + authorities: + type: array + items: + $ref: '#/components/schemas/GrantedAuthority' + username: + type: string + isAccountNonExpired: + type: boolean + isAccountNonLocked: + type: boolean + isCredentialsNonExpired: + type: boolean + PageEvent: + type: object + properties: + totalElements: + type: integer + format: int64 + totalPages: + type: integer + format: int32 + size: + type: integer + format: int32 + content: + type: array + items: + $ref: '#/components/schemas/Event' + number: + type: integer + format: int32 + sort: + $ref: '#/components/schemas/SortObject' + first: + type: boolean + last: + type: boolean + numberOfElements: + type: integer + format: int32 + pageable: + $ref: '#/components/schemas/PageableObject' + empty: + type: boolean + PageBookmark: + type: object + properties: + totalElements: + type: integer + format: int64 + totalPages: + type: integer + format: int32 + size: + type: integer + format: int32 + content: + type: array + items: + $ref: '#/components/schemas/Bookmark' + number: + type: integer + format: int32 + sort: + $ref: '#/components/schemas/SortObject' + first: + type: boolean + last: + type: boolean + numberOfElements: + type: integer + format: int32 + pageable: + $ref: '#/components/schemas/PageableObject' + empty: + type: boolean diff --git a/src/integrationTest/kotlin/com/group4/ticketingservice/Bookmark/BookmarkRepositoryTest.kt b/src/integrationTest/kotlin/com/group4/ticketingservice/Bookmark/BookmarkRepositoryTest.kt index cd351ad9..e4f74bd5 100644 --- a/src/integrationTest/kotlin/com/group4/ticketingservice/Bookmark/BookmarkRepositoryTest.kt +++ b/src/integrationTest/kotlin/com/group4/ticketingservice/Bookmark/BookmarkRepositoryTest.kt @@ -9,9 +9,13 @@ import com.group4.ticketingservice.repository.EventRepository import com.group4.ticketingservice.repository.UserRepository import com.group4.ticketingservice.utils.Authority import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertInstanceOf import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Pageable import org.springframework.data.repository.findByIdOrNull import org.springframework.transaction.annotation.Transactional import java.time.OffsetDateTime @@ -37,11 +41,49 @@ class BookmarkRepositoryTest( maxAttendees = 10 ) - private val sampleBookmark = Bookmark( - user = sampleUser, - event = sampleEvent + val sampleEvents = mutableListOf( + Event( + title = "정섭이의 코딩쇼", + date = OffsetDateTime.now(), + reservationEndTime = OffsetDateTime.now(), + reservationStartTime = OffsetDateTime.now(), + maxAttendees = 10 + ), + Event( + title = "민준이의 전국군가잘함", + date = OffsetDateTime.now(), + reservationEndTime = OffsetDateTime.now(), + reservationStartTime = OffsetDateTime.now(), + maxAttendees = 10 + ), + Event( + title = "하영이의 신작도서 팬싸인회", + date = OffsetDateTime.now(), + reservationEndTime = OffsetDateTime.now(), + reservationStartTime = OffsetDateTime.now(), + maxAttendees = 10 + ), + Event( + title = "준하의 스파르타 코딩 동아리 설명회", + date = OffsetDateTime.now(), + reservationEndTime = OffsetDateTime.now(), + reservationStartTime = OffsetDateTime.now(), + maxAttendees = 10 + ), + Event( + title = "군대에서 코딩 직군으로 복무하기 설명회", + date = OffsetDateTime.now(), + reservationEndTime = OffsetDateTime.now(), + reservationStartTime = OffsetDateTime.now(), + maxAttendees = 10 + ) ) + @AfterEach + fun removeBookmark() { + bookmarkRepository.deleteAll() + } + @Test fun `bookmarkRepository_save should return savedBookmark`() { // given @@ -67,7 +109,7 @@ class BookmarkRepositoryTest( val foundBookmark = bookmarkRepository.findByIdAndUserId(savedBookmark.id!!, savedUser.id!!) // then - assert(savedBookmark.id == foundBookmark.id) + assert(savedBookmark.id == foundBookmark?.id) } @Test @@ -93,12 +135,35 @@ class BookmarkRepositoryTest( val savedUser = userRepository.save(sampleUser) val savedEvent = eventRepository.save(sampleEvent) bookmarkRepository.save(Bookmark(user = savedUser, event = savedEvent)) + val pageable: Pageable = PageRequest.of(0, 10) + + // when + val listofBookmarks = bookmarkRepository.findByUserId(savedUser.id!!, pageable) + + // then + assertInstanceOf(Page::class.java, listofBookmarks) + assertInstanceOf(Bookmark::class.java, listofBookmarks.content[0]) + } + + @Test + fun `bookmarkRepository_findByUser should return page of bookmarks with pagination`() { + // given + val savedUser = userRepository.save(sampleUser) + bookmarkRepository.deleteAll() + sampleEvents.forEach { + val savedEvent = eventRepository.save(it) + bookmarkRepository.save(Bookmark(user = savedUser, event = savedEvent)) + } + val pageSize = 2 + val pageable: Pageable = PageRequest.of(0, pageSize) // when - val listofBookmarks = bookmarkRepository.findByUserId(savedUser.id!!) + val result = bookmarkRepository.findByUserId(savedUser.id!!, pageable) // then - assertInstanceOf(ArrayList::class.java, listofBookmarks) - assertInstanceOf(Bookmark::class.java, listofBookmarks[0]) + assertThat(result.totalElements).isEqualTo(sampleEvents.size.toLong()) + assertThat(result.pageable.pageSize).isEqualTo(pageSize) + assertThat(result.numberOfElements).isEqualTo(pageSize) + assertThat(result.content.size).isEqualTo(pageSize) } } diff --git a/src/integrationTest/kotlin/com/group4/ticketingservice/Event/EventRepositoryTest.kt b/src/integrationTest/kotlin/com/group4/ticketingservice/Event/EventRepositoryTest.kt index e8ba3240..f8da527b 100644 --- a/src/integrationTest/kotlin/com/group4/ticketingservice/Event/EventRepositoryTest.kt +++ b/src/integrationTest/kotlin/com/group4/ticketingservice/Event/EventRepositoryTest.kt @@ -2,21 +2,26 @@ package com.group4.ticketingservice.Event import com.group4.ticketingservice.AbstractIntegrationTest import com.group4.ticketingservice.Reservation.ReservationTest +import com.group4.ticketingservice.dto.EventSpecifications import com.group4.ticketingservice.entity.Event import com.group4.ticketingservice.entity.User import com.group4.ticketingservice.repository.EventRepository import com.group4.ticketingservice.repository.UserRepository import com.group4.ticketingservice.utils.Authority import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertInstanceOf import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Sort import org.springframework.data.repository.findByIdOrNull import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import java.time.Duration.ofHours import java.time.OffsetDateTime -import java.time.ZoneOffset class EventRepositoryTest @Autowired constructor( @Autowired val eventRepository: EventRepository, @@ -30,22 +35,69 @@ class EventRepositoryTest @Autowired constructor( authority = Authority.USER ) - @BeforeEach fun saveUser() { + val sampleEvent = Event( + title = "test title", + date = OffsetDateTime.now(), + reservationEndTime = OffsetDateTime.now() + ofHours(2), + reservationStartTime = OffsetDateTime.now() + ofHours(1), + maxAttendees = 10 + ) + + val sampleEvents = mutableListOf( + Event( + title = "정섭이의 코딩쇼", + date = OffsetDateTime.now(), + reservationEndTime = OffsetDateTime.now(), + reservationStartTime = OffsetDateTime.now(), + maxAttendees = 10 + ), + Event( + title = "민준이의 전국군가잘함", + date = OffsetDateTime.now(), + reservationEndTime = OffsetDateTime.now(), + reservationStartTime = OffsetDateTime.now(), + maxAttendees = 10 + ), + Event( + title = "하영이의 신작도서 팬싸인회", + date = OffsetDateTime.now(), + reservationEndTime = OffsetDateTime.now(), + reservationStartTime = OffsetDateTime.now(), + maxAttendees = 10 + ), + Event( + title = "준하의 스파르타 코딩 동아리 설명회", + date = OffsetDateTime.now(), + reservationEndTime = OffsetDateTime.now(), + reservationStartTime = OffsetDateTime.now(), + maxAttendees = 10 + ), + Event( + title = "군대에서 코딩 직군으로 복무하기 설명회", + date = OffsetDateTime.now(), + reservationEndTime = OffsetDateTime.now(), + reservationStartTime = OffsetDateTime.now(), + maxAttendees = 10 + ) + ) + + @BeforeEach + fun addData() { userRepository.save(sampleUser) + sampleEvents.forEach { + eventRepository.save(it) + } + } + + @AfterEach + fun removeData() { + userRepository.deleteAll() + eventRepository.deleteAll() } @Test fun `EventRepository_save should return savedEvent`() { // given - val now = OffsetDateTime.now() - val sampleEvent = Event( - title = "test title", - date = now, - reservationEndTime = now + ofHours(2), - reservationStartTime = now + ofHours(1), - maxAttendees = 10 - - ) // when val savedEvent = eventRepository.save(sampleEvent) @@ -57,15 +109,6 @@ class EventRepositoryTest @Autowired constructor( @Test fun `EventRepository_findByIdOrNull should return event`() { // given - val now = OffsetDateTime.now(ZoneOffset.UTC) - val sampleEvent = Event( - title = "test title", - date = now, - reservationEndTime = now + ofHours(2), - reservationStartTime = now + ofHours(1), - maxAttendees = 10 - - ) val savedEvent = eventRepository.save(sampleEvent) // when @@ -80,16 +123,6 @@ class EventRepositoryTest @Autowired constructor( @Test fun `EventRepository_findAll should return list of events`() { // given - val now = OffsetDateTime.now(ZoneOffset.UTC) - val sampleEvent = Event( - title = "test title", - date = now, - reservationEndTime = now + ofHours(2), - reservationStartTime = now + ofHours(1), - maxAttendees = 10 - - ) - eventRepository.save(sampleEvent) // when val events = eventRepository.findAll() @@ -100,17 +133,59 @@ class EventRepositoryTest @Autowired constructor( } @Test - fun `EventRepository_delete should delete event`() { + fun `EventRepository_findAll should return page of events with searching`() { // given - val now = OffsetDateTime.now(ZoneOffset.UTC) - val sampleEvent = Event( - title = "test title", - date = now, - reservationEndTime = now + ofHours(2), - reservationStartTime = now + ofHours(1), - maxAttendees = 10 + val pageSize = 10 + val pageable: Pageable = PageRequest.of(0, pageSize) + val title = "코딩" + val specification = EventSpecifications.withTitle(title) + val `totalElementsTitleLike코딩`: Long = 3 - ) + // when + val result = eventRepository.findAll(specification, pageable) + + // then + assertThat(result.totalElements).isEqualTo(`totalElementsTitleLike코딩`) + } + + @Test + fun `EventRepository_findAll should return page of events with pagination`() { + // given + val pageSize = 2 + val pageable: Pageable = PageRequest.of(0, pageSize) + + // when + val result = eventRepository.findAll(pageable) + + // then + assertThat(result.totalElements).isEqualTo(sampleEvents.size.toLong()) + assertThat(result.pageable.pageSize).isEqualTo(pageSize) + assertThat(result.numberOfElements).isEqualTo(pageSize) + assertThat(result.content.size).isEqualTo(pageSize) + } + + @Test + fun `EventRepository_findAll should return page of events with sorting`() { + // given + val pageSize = 10 + val pageable: Pageable = PageRequest.of(0, pageSize, Sort.by("title").ascending()) + val sortedItemIndexs = mutableListOf(4, 1, 0, 3, 2) + + // when + val result = eventRepository.findAll(pageable) + + // then + assertThat(result.totalElements).isEqualTo(sampleEvents.size.toLong()) + assertThat(result.content[0].title).isEqualTo(sampleEvents[sortedItemIndexs[0]].title) + assertThat(result.content[1].title).isEqualTo(sampleEvents[sortedItemIndexs[1]].title) + assertThat(result.content[2].title).isEqualTo(sampleEvents[sortedItemIndexs[2]].title) + assertThat(result.content[3].title).isEqualTo(sampleEvents[sortedItemIndexs[3]].title) + assertThat(result.content[4].title).isEqualTo(sampleEvents[sortedItemIndexs[4]].title) + } + + @Test + fun `EventRepository_delete should delete event`() { + // given val savedEvent = eventRepository.save(sampleEvent) // when diff --git a/src/integrationTest/kotlin/com/group4/ticketingservice/ResponseFormatTest.kt b/src/integrationTest/kotlin/com/group4/ticketingservice/ResponseFormatTest.kt index 07e2d621..5b263a29 100644 --- a/src/integrationTest/kotlin/com/group4/ticketingservice/ResponseFormatTest.kt +++ b/src/integrationTest/kotlin/com/group4/ticketingservice/ResponseFormatTest.kt @@ -32,7 +32,6 @@ class ResponseFormatTest : AbstractIntegrationTest() { assertEquals(HttpStatus.NOT_FOUND.value(), response.status) val errorResponse = objectMapper.readValue(response.contentAsString, ErrorResponseDTO::class.java) - assertNotNull(errorResponse.timestamp) assertEquals(ErrorCodes.END_POINT_NOT_FOUND.errorCode, errorResponse.errorCode) assertNotNull(errorResponse.message) diff --git a/src/integrationTest/kotlin/com/group4/ticketingservice/TimeE2ETest.kt b/src/integrationTest/kotlin/com/group4/ticketingservice/TimeE2ETest.kt index c85d2b02..081f64f8 100644 --- a/src/integrationTest/kotlin/com/group4/ticketingservice/TimeE2ETest.kt +++ b/src/integrationTest/kotlin/com/group4/ticketingservice/TimeE2ETest.kt @@ -69,10 +69,10 @@ class TimeE2ETest @Autowired constructor( .contentType(MediaType.APPLICATION_JSON) .content(eventCreateRequest) ) - .andExpect(status().isOk) + .andExpect(status().isCreated) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$.date").value("2044-02-04T12:00:00.001Z")) - .andExpect(jsonPath("$.reservationStartTime").value("2044-01-01T13:00:00.001Z")) - .andExpect(jsonPath("$.reservationEndTime").value("2044-01-01T14:00:00.001Z")) + .andExpect(jsonPath("$.data.date").value("2044-02-04T12:00:00.001Z")) + .andExpect(jsonPath("$.data.reservationStartTime").value("2044-01-01T13:00:00.001Z")) + .andExpect(jsonPath("$.data.reservationEndTime").value("2044-01-01T14:00:00.001Z")) } } diff --git a/src/integrationTest/kotlin/com/group4/ticketingservice/User/UserControllerTest.kt b/src/integrationTest/kotlin/com/group4/ticketingservice/User/UserControllerTest.kt index ade21ded..39f8f7ec 100644 --- a/src/integrationTest/kotlin/com/group4/ticketingservice/User/UserControllerTest.kt +++ b/src/integrationTest/kotlin/com/group4/ticketingservice/User/UserControllerTest.kt @@ -121,8 +121,8 @@ class UserControllerTest : AbstractIntegrationTest() { .header("Authorization", jwt) ) resultActions.andExpect(status().isOk) - .andExpect(MockMvcResultMatchers.jsonPath("$.expires_in").exists()) - .andExpect(MockMvcResultMatchers.jsonPath("$.userId").exists()) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.expires_in").exists()) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.userId").exists()) } @Test diff --git a/src/integrationTest/kotlin/com/group4/ticketingservice/actuator/ActuatorTest.kt b/src/integrationTest/kotlin/com/group4/ticketingservice/actuator/ActuatorTest.kt index 9214a887..78b995c8 100644 --- a/src/integrationTest/kotlin/com/group4/ticketingservice/actuator/ActuatorTest.kt +++ b/src/integrationTest/kotlin/com/group4/ticketingservice/actuator/ActuatorTest.kt @@ -18,6 +18,6 @@ class ActuatorTest : AbstractIntegrationTest() { mockMvc.perform( MockMvcRequestBuilders.get("/actuator") ).andExpect(MockMvcResultMatchers.status().isOk) - .andExpect(MockMvcResultMatchers.jsonPath("$._links").exists()) + .andExpect(MockMvcResultMatchers.jsonPath("$.data._links").exists()) } } diff --git a/src/integrationTest/resources/application.properties b/src/integrationTest/resources/application.properties index 6bfb323e..47827ee6 100644 --- a/src/integrationTest/resources/application.properties +++ b/src/integrationTest/resources/application.properties @@ -9,4 +9,6 @@ ticketing.jwt.expiration-hours=24 ticketing.jwt.issuer=minjun management.endpoints.prometheus.enabled=true -management.endpoints.web.exposure.include=prometheus \ No newline at end of file +management.endpoints.web.exposure.include=prometheus + +spring.data.web.pageable.one-indexed-parameters=true \ No newline at end of file diff --git a/src/main/kotlin/com/group4/ticketingservice/controller/BookmarkController.kt b/src/main/kotlin/com/group4/ticketingservice/controller/BookmarkController.kt index bd66af72..5a35dd93 100644 --- a/src/main/kotlin/com/group4/ticketingservice/controller/BookmarkController.kt +++ b/src/main/kotlin/com/group4/ticketingservice/controller/BookmarkController.kt @@ -1,14 +1,18 @@ package com.group4.ticketingservice.controller import com.group4.ticketingservice.dto.BookmarkFromdto +import com.group4.ticketingservice.entity.Bookmark import com.group4.ticketingservice.service.BookmarkService +import jakarta.servlet.http.HttpServletRequest import jakarta.validation.Valid import org.springframework.beans.factory.annotation.Autowired +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.web.PageableDefault import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.security.core.annotation.AuthenticationPrincipal -import org.springframework.web.bind.MethodArgumentNotValidException import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable @@ -21,41 +25,54 @@ import org.springframework.web.bind.annotation.RestController @RequestMapping("bookmarks") class BookmarkController @Autowired constructor(val bookmarkService: BookmarkService) { - // 북마크 등록 @PostMapping fun addBookmark( @AuthenticationPrincipal userId: Int, @RequestBody @Valid boardFormDto: BookmarkFromdto ): ResponseEntity { - val savedBookmarkId = bookmarkService.create(userId, boardFormDto) + val savedBookmark: Bookmark = bookmarkService.create(userId, boardFormDto) + val headers = HttpHeaders() - headers.set("Content-Location", "/bookmark/%d".format(savedBookmarkId)) - return ResponseEntity.status(HttpStatus.CREATED).headers(headers).body(savedBookmarkId) + headers.set("Content-Location", "/bookmarks/%d".format(savedBookmark.id!!)) + + return ResponseEntity(savedBookmark, headers, HttpStatus.CREATED) } - // 특정 북마크 조회하기 @GetMapping("/{id}") - fun getBookmark(@AuthenticationPrincipal userId: Int, @PathVariable id: Int): ResponseEntity { - try { - val foundBookmark = bookmarkService.get(userId, id) - return ResponseEntity.status(HttpStatus.OK).body(foundBookmark ?: "null") - } catch (e: MethodArgumentNotValidException) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).build() - } + fun getBookmark( + request: HttpServletRequest, + @AuthenticationPrincipal userId: Int, + @PathVariable id: Int + ): ResponseEntity { + val foundBookmark = bookmarkService.get(userId, id) + + val headers = HttpHeaders() + headers.set("Content-Location", request.requestURI) + + return ResponseEntity(foundBookmark, headers, HttpStatus.OK) } - // 북마크 삭제 @DeleteMapping("/{id}") - fun deleteBookmark(@AuthenticationPrincipal userId: Int, @PathVariable id: Int): ResponseEntity { + fun deleteBookmark( + @AuthenticationPrincipal userId: Int, + @PathVariable id: Int + ): ResponseEntity { bookmarkService.delete(userId, id) - return ResponseEntity.status(HttpStatus.NO_CONTENT).build() + return ResponseEntity(null, HttpStatus.OK) } - // 로그인한 사용자의 북마크 목록 - @GetMapping() - fun getBookmarks(@AuthenticationPrincipal userId: Int): ResponseEntity { - val bookmarks = bookmarkService.getList(userId) - return ResponseEntity.status(HttpStatus.OK).body(bookmarks) + @GetMapping + fun getBookmarks( + request: HttpServletRequest, + @AuthenticationPrincipal userId: Int, + @PageableDefault(size = 10) pageable: Pageable + ): ResponseEntity> { + val page = bookmarkService.getBookmarks(userId, pageable) + + val headers = HttpHeaders() + headers.set("Content-Location", request.requestURI) + + return ResponseEntity(page, headers, HttpStatus.OK) } } diff --git a/src/main/kotlin/com/group4/ticketingservice/controller/EventController.kt b/src/main/kotlin/com/group4/ticketingservice/controller/EventController.kt index dd2fd765..e123dc3c 100644 --- a/src/main/kotlin/com/group4/ticketingservice/controller/EventController.kt +++ b/src/main/kotlin/com/group4/ticketingservice/controller/EventController.kt @@ -2,9 +2,15 @@ package com.group4.ticketingservice.controller import com.group4.ticketingservice.dto.EventCreateRequest import com.group4.ticketingservice.dto.EventResponse +import com.group4.ticketingservice.entity.Event import com.group4.ticketingservice.service.EventService +import jakarta.servlet.http.HttpServletRequest import jakarta.validation.Valid import org.springframework.beans.factory.annotation.Autowired +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.web.PageableDefault +import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping @@ -12,6 +18,7 @@ import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @RestController @@ -33,6 +40,7 @@ class EventController @Autowired constructor( request.reservationEndTime!!, request.maxAttendees!! ) + val response = EventResponse( id = event.id!!, title = event.title, @@ -41,31 +49,19 @@ class EventController @Autowired constructor( reservationEndTime = event.reservationEndTime, maxAttendees = event.maxAttendees ) - return ResponseEntity.status(HttpStatus.OK).body(response) - } - @GetMapping("/{id}") - fun getEvent(@PathVariable id: Int): ResponseEntity { - return eventService.getEvent(id)?.let { - ResponseEntity.status(HttpStatus.OK).body( - EventResponse( - id = it.id!!, - title = it.title, - date = it.date, - reservationStartTime = it.reservationStartTime, - reservationEndTime = it.reservationEndTime, - maxAttendees = it.maxAttendees - ) - ) - } ?: kotlin.run { - ResponseEntity.status(HttpStatus.OK).body(null) - } + val headers = HttpHeaders() + headers.set("Content-Location", "/events/%d".format(event.id!!)) + + return ResponseEntity(response, headers, HttpStatus.CREATED) } - @GetMapping("/") - fun getEvents(): ResponseEntity> { - val events = eventService.getEvents() - val response: List = events.map { + @GetMapping("/{id}") + fun getEvent( + request: HttpServletRequest, + @PathVariable id: Int + ): ResponseEntity { + val foundEvent = eventService.getEvent(id)?.let { EventResponse( id = it.id!!, title = it.title, @@ -74,11 +70,27 @@ class EventController @Autowired constructor( reservationEndTime = it.reservationEndTime, maxAttendees = it.maxAttendees ) + } ?: kotlin.run { + null } - return if (events.isEmpty()) { - ResponseEntity.status(HttpStatus.NO_CONTENT).body(response) - } else { - ResponseEntity.status(HttpStatus.OK).body(response) - } + + val headers = HttpHeaders() + headers.set("Content-Location", request.requestURI) + + return ResponseEntity(foundEvent, headers, HttpStatus.OK) + } + + @GetMapping + fun getEvents( + request: HttpServletRequest, + @RequestParam(required = false) title: String?, + @PageableDefault(size = 10, sort = ["date", "id"]) pageable: Pageable + ): ResponseEntity> { + val page = eventService.getEvents(title, pageable) + + val headers = HttpHeaders() + headers.set("Content-Location", request.requestURI) + + return ResponseEntity(page, headers, HttpStatus.OK) } } diff --git a/src/main/kotlin/com/group4/ticketingservice/controller/HealthController.kt b/src/main/kotlin/com/group4/ticketingservice/controller/HealthController.kt index a0181a28..a1ec1ddd 100644 --- a/src/main/kotlin/com/group4/ticketingservice/controller/HealthController.kt +++ b/src/main/kotlin/com/group4/ticketingservice/controller/HealthController.kt @@ -1,5 +1,6 @@ package com.group4.ticketingservice.controller +import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping @@ -12,11 +13,17 @@ class HealthController { @GetMapping() fun healthCheck1(): ResponseEntity { - return ResponseEntity.status(HttpStatus.OK).body("OK") + val headers = HttpHeaders() + headers.set("Content-Location", "/health") + + return ResponseEntity("OK", headers, HttpStatus.OK) } @GetMapping("/health") fun healthCheck2(): ResponseEntity { - return ResponseEntity.status(HttpStatus.OK).body("OK") + val headers = HttpHeaders() + headers.set("Content-Location", "/health") + + return ResponseEntity("OK", headers, HttpStatus.OK) } } diff --git a/src/main/kotlin/com/group4/ticketingservice/controller/ReservationController.kt b/src/main/kotlin/com/group4/ticketingservice/controller/ReservationController.kt index c9982adb..1eaab2ec 100644 --- a/src/main/kotlin/com/group4/ticketingservice/controller/ReservationController.kt +++ b/src/main/kotlin/com/group4/ticketingservice/controller/ReservationController.kt @@ -5,7 +5,13 @@ import com.group4.ticketingservice.dto.ReservationResponse import com.group4.ticketingservice.dto.ReservationUpdateRequest import com.group4.ticketingservice.entity.Reservation import com.group4.ticketingservice.service.ReservationService +import jakarta.servlet.http.HttpServletRequest import jakarta.validation.Valid +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable +import org.springframework.data.web.PageableDefault +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.security.core.annotation.AuthenticationPrincipal import org.springframework.web.bind.annotation.DeleteMapping @@ -30,46 +36,82 @@ class ReservationController(val reservationService: ReservationService) { request.eventId!!, userId ) + val response = ReservationResponse( id = reservation.id!!, eventId = reservation.event.id!!, userId = reservation.user.id!!, bookedAt = reservation.bookedAt ) - return ResponseEntity.ok(response) + + val headers = HttpHeaders() + headers.set("Content-Location", "/reservations/%d".format(reservation.id!!)) + + return ResponseEntity(response, headers, HttpStatus.CREATED) } @GetMapping("/{id}") - fun getReservation(@PathVariable id: Int): ResponseEntity { - val reservation = reservationService.getReservation(id) + fun getReservation( + request: HttpServletRequest, + @PathVariable id: Int + ): ResponseEntity { + val foundReservation = reservationService.getReservation(id) + val response = ReservationResponse( - id = reservation.id!!, - eventId = reservation.event.id!!, - userId = reservation.user.id!!, - bookedAt = reservation.bookedAt + id = foundReservation.id!!, + eventId = foundReservation.event.id!!, + userId = foundReservation.user.id!!, + bookedAt = foundReservation.bookedAt ) - return ResponseEntity.ok(response) + + val headers = HttpHeaders() + headers.set("Content-Location", request.requestURI) + + return ResponseEntity(response, headers, HttpStatus.OK) } @PutMapping("/{id}") fun updateReservation( + request: HttpServletRequest, @PathVariable id: Int, @RequestBody @Valid - request: ReservationUpdateRequest + reservationRequest: ReservationUpdateRequest ): ResponseEntity { - val reservation = reservationService.updateReservation(id, request.eventId!!) + val reservation = reservationService.updateReservation(id, reservationRequest.eventId!!) + val response = ReservationResponse( id = reservation.id!!, eventId = reservation.event.id!!, userId = reservation.user.id!!, bookedAt = reservation.bookedAt ) - return ResponseEntity.ok(response) + + val headers = HttpHeaders() + headers.set("Content-Location", request.requestURI) + + return ResponseEntity(response, headers, HttpStatus.OK) } @DeleteMapping("/{id}") - fun deleteReservation(@AuthenticationPrincipal userId: Int, @PathVariable id: Int): ResponseEntity { + fun deleteReservation( + @AuthenticationPrincipal userId: Int, + @PathVariable id: Int + ): ResponseEntity { reservationService.deleteReservation(userId, id) - return ResponseEntity.noContent().build() + return ResponseEntity(null, HttpStatus.OK) + } + + @GetMapping + fun getReservations( + request: HttpServletRequest, + @AuthenticationPrincipal userId: Int, + @PageableDefault(size = 10) pageable: Pageable + ): ResponseEntity> { + val page = reservationService.getReservations(userId, pageable) + + val headers = HttpHeaders() + headers.set("Content-Location", request.requestURI) + + return ResponseEntity(page, headers, HttpStatus.OK) } } diff --git a/src/main/kotlin/com/group4/ticketingservice/controller/UserController.kt b/src/main/kotlin/com/group4/ticketingservice/controller/UserController.kt index 95a2ca58..aff6e359 100644 --- a/src/main/kotlin/com/group4/ticketingservice/controller/UserController.kt +++ b/src/main/kotlin/com/group4/ticketingservice/controller/UserController.kt @@ -4,7 +4,9 @@ import com.group4.ticketingservice.dto.SignUpRequest import com.group4.ticketingservice.dto.UserDto import com.group4.ticketingservice.service.UserService import com.group4.ticketingservice.utils.TokenProvider +import jakarta.servlet.http.HttpServletRequest import jakarta.validation.Valid +import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.security.core.annotation.AuthenticationPrincipal @@ -43,13 +45,20 @@ class UserController( * @author MinJun Kim */ @GetMapping("/access_token_info") - fun getAccessTokenInfo(@AuthenticationPrincipal userId: Int): ResponseEntity> { + fun getAccessTokenInfo( + request: HttpServletRequest, + @AuthenticationPrincipal userId: Int + ): ResponseEntity> { val jwt = SecurityContextHolder.getContext().authentication.credentials.toString() val expiresInMillis = tokenProvider.parseTokenExpirationTime(jwt) val map = mapOf( "userId" to userId, "expires_in" to expiresInMillis ) - return ResponseEntity.ok(map) + + val headers = HttpHeaders() + headers.set("Content-Location", request.requestURI) + + return ResponseEntity(map, headers, HttpStatus.OK) } } diff --git a/src/main/kotlin/com/group4/ticketingservice/dto/EventSpecifications.kt b/src/main/kotlin/com/group4/ticketingservice/dto/EventSpecifications.kt new file mode 100644 index 00000000..a75d8bcf --- /dev/null +++ b/src/main/kotlin/com/group4/ticketingservice/dto/EventSpecifications.kt @@ -0,0 +1,24 @@ +package com.group4.ticketingservice.dto + +import com.group4.ticketingservice.entity.Event +import jakarta.persistence.criteria.CriteriaBuilder +import jakarta.persistence.criteria.CriteriaQuery +import jakarta.persistence.criteria.Predicate +import jakarta.persistence.criteria.Root +import org.springframework.data.jpa.domain.Specification + +class EventSpecifications { + companion object { + fun withTitle(title: String?): Specification { + return Specification { root: Root, query: CriteriaQuery<*>, criteriaBuilder: CriteriaBuilder -> + val predicates = mutableListOf() + + if (!title.isNullOrBlank()) { + predicates.add(criteriaBuilder.like(criteriaBuilder.lower(root.get("title")), "%${title.toLowerCase()}%")) + } + + criteriaBuilder.and(*predicates.toTypedArray()) + } + } + } +} diff --git a/src/main/kotlin/com/group4/ticketingservice/dto/ResponseDTO.kt b/src/main/kotlin/com/group4/ticketingservice/dto/ResponseDTO.kt index 57d95163..b08e500a 100644 --- a/src/main/kotlin/com/group4/ticketingservice/dto/ResponseDTO.kt +++ b/src/main/kotlin/com/group4/ticketingservice/dto/ResponseDTO.kt @@ -4,10 +4,10 @@ import java.time.OffsetDateTime data class SuccessResponseDTO( val timestamp: OffsetDateTime = OffsetDateTime.now(), - val message: String, - val data: Any, - val path: String - + val message: String = "success", + var data: Any? = null, + val path: String? = null, + val totalElements: Long? = null ) data class ErrorResponseDTO( diff --git a/src/main/kotlin/com/group4/ticketingservice/filter/JwtAuthenticationFilter.kt b/src/main/kotlin/com/group4/ticketingservice/filter/JwtAuthenticationFilter.kt index 10d0fd9e..de8bc5e5 100644 --- a/src/main/kotlin/com/group4/ticketingservice/filter/JwtAuthenticationFilter.kt +++ b/src/main/kotlin/com/group4/ticketingservice/filter/JwtAuthenticationFilter.kt @@ -2,18 +2,15 @@ package com.group4.ticketingservice.filter import com.fasterxml.jackson.databind.ObjectMapper import com.google.gson.Gson -import com.group4.ticketingservice.dto.ErrorResponseDTO import com.group4.ticketingservice.dto.SignInRequest import com.group4.ticketingservice.entity.User import com.group4.ticketingservice.utils.TokenProvider -import com.group4.ticketingservice.utils.exception.ErrorCodes import jakarta.servlet.FilterChain import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.core.Authentication -import org.springframework.security.core.AuthenticationException import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter import java.io.PrintWriter @@ -51,23 +48,4 @@ class JwtAuthenticationFilter( val writer: PrintWriter = response.writer writer.println(body) } - - override fun unsuccessfulAuthentication( - request: HttpServletRequest, - response: HttpServletResponse, - failed: AuthenticationException? - ) { - val errorCode = ErrorCodes.LOGIN_FAIL - val errorDto = ErrorResponseDTO( - errorCode = errorCode.errorCode, - message = errorCode.message, - path = request.requestURI - ) - - val body = gson.toJson(errorDto) - response.contentType = "application/json;charset=UTF-8" - response.status = HttpServletResponse.SC_UNAUTHORIZED - val writer: PrintWriter = response.writer - writer.println(body) - } } diff --git a/src/main/kotlin/com/group4/ticketingservice/repository/BookmarkRepository.kt b/src/main/kotlin/com/group4/ticketingservice/repository/BookmarkRepository.kt index 25a8669f..8702720d 100644 --- a/src/main/kotlin/com/group4/ticketingservice/repository/BookmarkRepository.kt +++ b/src/main/kotlin/com/group4/ticketingservice/repository/BookmarkRepository.kt @@ -1,12 +1,14 @@ package com.group4.ticketingservice.repository import com.group4.ticketingservice.entity.Bookmark +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository @Repository interface BookmarkRepository : JpaRepository { - fun findByIdAndUserId(id: Int, userId: Int): Bookmark + fun findByIdAndUserId(id: Int, userId: Int): Bookmark? fun deleteByIdAndUserId(id: Int, userId: Int) - fun findByUserId(userId: Int): List + fun findByUserId(userId: Int, pageable: Pageable): Page } diff --git a/src/main/kotlin/com/group4/ticketingservice/repository/EventRepository.kt b/src/main/kotlin/com/group4/ticketingservice/repository/EventRepository.kt index 38a85ccd..eb6e7544 100644 --- a/src/main/kotlin/com/group4/ticketingservice/repository/EventRepository.kt +++ b/src/main/kotlin/com/group4/ticketingservice/repository/EventRepository.kt @@ -3,12 +3,13 @@ package com.group4.ticketingservice.repository import com.group4.ticketingservice.entity.Event import jakarta.persistence.LockModeType import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.JpaSpecificationExecutor import org.springframework.data.jpa.repository.Lock import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository @Repository -interface EventRepository : JpaRepository { +interface EventRepository : JpaRepository, JpaSpecificationExecutor { @Lock(value = LockModeType.PESSIMISTIC_WRITE) @Query("select e from Event e where e.id = :id") fun findByIdWithPesimisticLock(id: Int): Event? diff --git a/src/main/kotlin/com/group4/ticketingservice/repository/ReservationRepository.kt b/src/main/kotlin/com/group4/ticketingservice/repository/ReservationRepository.kt index 379164a1..136d918f 100644 --- a/src/main/kotlin/com/group4/ticketingservice/repository/ReservationRepository.kt +++ b/src/main/kotlin/com/group4/ticketingservice/repository/ReservationRepository.kt @@ -1,8 +1,12 @@ package com.group4.ticketingservice.repository import com.group4.ticketingservice.entity.Reservation +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository @Repository -interface ReservationRepository : JpaRepository +interface ReservationRepository : JpaRepository { + fun findByUserId(userId: Int, pageable: Pageable): Page +} diff --git a/src/main/kotlin/com/group4/ticketingservice/service/BookmarkService.kt b/src/main/kotlin/com/group4/ticketingservice/service/BookmarkService.kt index 003908f1..908666fd 100644 --- a/src/main/kotlin/com/group4/ticketingservice/service/BookmarkService.kt +++ b/src/main/kotlin/com/group4/ticketingservice/service/BookmarkService.kt @@ -7,8 +7,13 @@ import com.group4.ticketingservice.entity.User import com.group4.ticketingservice.repository.BookmarkRepository import com.group4.ticketingservice.repository.EventRepository import com.group4.ticketingservice.repository.UserRepository +import com.group4.ticketingservice.utils.exception.CustomException +import com.group4.ticketingservice.utils.exception.ErrorCodes import org.springframework.beans.factory.annotation.Autowired +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional @Service class BookmarkService @Autowired constructor( @@ -17,25 +22,26 @@ class BookmarkService @Autowired constructor( val bookmarkRepository: BookmarkRepository ) { - fun create(userId: Int, bookmarkFormDto: BookmarkFromdto): Int? { + fun create(userId: Int, bookmarkFormDto: BookmarkFromdto): Bookmark { val user: User = userRepository.getReferenceById(userId) val event: Event = eventRepository.getReferenceById(bookmarkFormDto.event_id!!) val bookmark = Bookmark(user = user, event = event) - return bookmarkRepository.save(bookmark).id + return bookmarkRepository.save(bookmark) } fun get(userId: Int, id: Int): Bookmark? { - return bookmarkRepository.findByIdAndUserId(id, userId) + return bookmarkRepository.findByIdAndUserId(id, userId) ?: throw CustomException(ErrorCodes.END_POINT_NOT_FOUND) } + @Transactional fun delete(userId: Int, id: Int) { bookmarkRepository.deleteByIdAndUserId(id, userId) } - fun getList(userId: Int): List { - return bookmarkRepository.findByUserId(userId) + fun getBookmarks(userId: Int, pageable: Pageable): Page { + return bookmarkRepository.findByUserId(userId, pageable) } } diff --git a/src/main/kotlin/com/group4/ticketingservice/service/EventService.kt b/src/main/kotlin/com/group4/ticketingservice/service/EventService.kt index 5e66dea7..7590f6a1 100644 --- a/src/main/kotlin/com/group4/ticketingservice/service/EventService.kt +++ b/src/main/kotlin/com/group4/ticketingservice/service/EventService.kt @@ -1,7 +1,10 @@ package com.group4.ticketingservice.service +import com.group4.ticketingservice.dto.EventSpecifications import com.group4.ticketingservice.entity.Event import com.group4.ticketingservice.repository.EventRepository +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable import org.springframework.stereotype.Service import java.time.OffsetDateTime @@ -30,7 +33,8 @@ class EventService( return eventRepository.findById(id).orElse(null) } - fun getEvents(): List { - return eventRepository.findAll() + fun getEvents(title: String?, pageable: Pageable): Page { + val specification = EventSpecifications.withTitle(title) + return eventRepository.findAll(specification, pageable) } } diff --git a/src/main/kotlin/com/group4/ticketingservice/service/ReservationService.kt b/src/main/kotlin/com/group4/ticketingservice/service/ReservationService.kt index e7fc2b71..6176f72e 100644 --- a/src/main/kotlin/com/group4/ticketingservice/service/ReservationService.kt +++ b/src/main/kotlin/com/group4/ticketingservice/service/ReservationService.kt @@ -7,6 +7,8 @@ import com.group4.ticketingservice.repository.UserRepository import com.group4.ticketingservice.utils.exception.CustomException import com.group4.ticketingservice.utils.exception.ErrorCodes import org.springframework.beans.factory.annotation.Autowired +import org.springframework.data.domain.Page +import org.springframework.data.domain.Pageable import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -59,4 +61,8 @@ class ReservationService @Autowired constructor( if (reservation.user.id != userId) throw CustomException(ErrorCodes.FORBIDDEN) reservationRepository.delete(reservation) } + + fun getReservations(userId: Int, pageable: Pageable): Page { + return reservationRepository.findByUserId(userId, pageable) + } } diff --git a/src/main/kotlin/com/group4/ticketingservice/utils/PageSerializer.kt b/src/main/kotlin/com/group4/ticketingservice/utils/PageSerializer.kt new file mode 100644 index 00000000..0f49ffa7 --- /dev/null +++ b/src/main/kotlin/com/group4/ticketingservice/utils/PageSerializer.kt @@ -0,0 +1,21 @@ +package com.matchilling.api.rest.data + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.databind.JsonSerializer +import com.fasterxml.jackson.databind.SerializerProvider +import org.springframework.boot.jackson.JsonComponent +import org.springframework.data.domain.PageImpl +import java.io.IOException + +@JsonComponent +class PageSerializer : JsonSerializer>() { + + @Throws(IOException::class) + override fun serialize( + page: PageImpl<*>, + jsonGenerator: JsonGenerator, + serializerProvider: SerializerProvider + ) { + jsonGenerator.writeObject(page.content) + } +} diff --git a/src/main/kotlin/com/group4/ticketingservice/utils/ResponseAdvice.kt b/src/main/kotlin/com/group4/ticketingservice/utils/ResponseAdvice.kt new file mode 100644 index 00000000..5879519f --- /dev/null +++ b/src/main/kotlin/com/group4/ticketingservice/utils/ResponseAdvice.kt @@ -0,0 +1,177 @@ +package com.group4.ticketingservice.utils + +import com.group4.ticketingservice.controller.HealthController +import com.group4.ticketingservice.dto.ErrorResponseDTO +import com.group4.ticketingservice.dto.EventResponse +import com.group4.ticketingservice.dto.ReservationResponse +import com.group4.ticketingservice.dto.SuccessResponseDTO +import com.group4.ticketingservice.entity.Event +import com.group4.ticketingservice.entity.Reservation +import org.springframework.beans.factory.annotation.Value +import org.springframework.core.MethodParameter +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.Pageable +import org.springframework.http.MediaType +import org.springframework.http.converter.HttpMessageConverter +import org.springframework.http.server.ServerHttpRequest +import org.springframework.http.server.ServerHttpResponse +import org.springframework.web.bind.annotation.RestControllerAdvice +import org.springframework.web.context.request.RequestContextHolder +import org.springframework.web.context.request.ServletRequestAttributes +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice +import org.springframework.web.util.UriComponentsBuilder + +@RestControllerAdvice +class ResponseAdvice( + @Value("\${spring.data.web.pageable.one-indexed-parameters}") + private val oneIndexed: Boolean +) : ResponseBodyAdvice { + + override fun supports( + returnType: MethodParameter, + converterType: Class> + ): Boolean { + if (returnType.declaringClass == HealthController::class.java) return false + + val path = (RequestContextHolder.getRequestAttributes() as ServletRequestAttributes).request.servletPath + if (path.startsWith("/error"))return false + if (path.startsWith("/actuator"))return false + if (path.startsWith("/api-docs.yaml"))return false + + return true + } + + override fun beforeBodyWrite( + body: T?, + returnType: MethodParameter, + selectedContentType: MediaType, + selectedConverterType: Class>, + request: ServerHttpRequest, + response: ServerHttpResponse + ): T? { + if (body == null) { + return SuccessResponseDTO( + data = null, + path = response.headers.getFirst("Content-Location") + ) as T? + } + + if (body is ErrorResponseDTO) { + return body + } + + if (body !is Page<*>) { + return SuccessResponseDTO( + data = body as Any, + path = response.headers.getFirst("Content-Location") + ) as T? + } + + val page = PageImpl(body.content, body.pageable, body.totalElements) + + val headers = response.headers + headers.set( + "Access-Control-Expose-Headers", + "Link,Page-Number,Page-Size,Total-Elements,Total-Pages" + ) + + val links = page.links(request) + if (links.isNotBlank()) { + headers.set("Link", links) + } + + val pageNumber = if (oneIndexed) { + page.number.plus(1) + } else { + page.number + } + + headers.set("Page-Number", pageNumber.toString()) + headers.set("Page-Size", page.size.toString()) + headers.set("Total-Elements", page.totalElements.toString()) + headers.set("Total-Pages", page.totalPages.toString()) + + var data = page.content + + if (data.isEmpty()) { + data = listOf() + } else if (data[0] is Event) { + data = (page.content as List).map { + EventResponse( + id = it.id!!, + title = it.title, + date = it.date, + reservationStartTime = it.reservationStartTime, + reservationEndTime = it.reservationEndTime, + maxAttendees = it.maxAttendees + ) + } + } else if (data[0] is Reservation) { + data = (page.content as List).map { + ReservationResponse( + id = it.id!!, + eventId = it.event.id!!, + userId = it.user.id!!, + bookedAt = it.bookedAt + ) + } + } + + return SuccessResponseDTO( + data = data, + path = response.headers.getFirst("Content-Location"), + totalElements = page.totalElements + ) as T? + // return page as T? + } + + private fun PageImpl<*>.links(request: ServerHttpRequest): String { + val links = mutableListOf() + val builder = UriComponentsBuilder.fromUri(request.uri) + if (request.uri.host == "localhost") { + builder.port(request.uri.port) + } + + if (!this.isFirst) { + val link = builder.replacePageAndSize(this.pageable.first()) + links.add("<${link.toUriString()}>; rel=\"first\"") + } + + if (this.hasPrevious()) { + val link = builder.replacePageAndSize(this.previousPageable()) + links.add("<${link.toUriString()}>; rel=\"prev\"") + } + + if (this.hasNext()) { + val link = builder.replacePageAndSize(this.nextPageable()) + links.add("<${link.toUriString()}>; rel=\"next\"") + } + + if (!this.isLast) { + val last = builder.cloneBuilder() + last.replaceQueryParam("page", this.totalPages) + last.replaceQueryParam("size", this.size) + + links.add("<${last.toUriString()}>; rel=\"last\"") + } + + return links.joinToString(",") + } + + private fun UriComponentsBuilder.replacePageAndSize( + page: Pageable + ): UriComponentsBuilder { + val builder = this.cloneBuilder() + + val pageNumber = if (oneIndexed) { + page.pageNumber.plus(1) + } else { + page.pageNumber + } + builder.replaceQueryParam("page", pageNumber) + builder.replaceQueryParam("size", page.pageSize) + + return builder + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 9ace31c2..8d6ddc1c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -15,8 +15,7 @@ spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.MySQLDialect spring.security.user.name=user spring.security.user.password=1234 - - +spring.data.web.pageable.one-indexed-parameters=true ticketing.jwt.secret=${JWT_SECRET} ticketing.jwt.expiration-hours=${JWT_EXPIRATION_HOURS} diff --git a/src/test/kotlin/com/group4/ticketingservice/bookmark/BookmarkControllerTest.kt b/src/test/kotlin/com/group4/ticketingservice/bookmark/BookmarkControllerTest.kt index 41ee4595..a0c46aed 100644 --- a/src/test/kotlin/com/group4/ticketingservice/bookmark/BookmarkControllerTest.kt +++ b/src/test/kotlin/com/group4/ticketingservice/bookmark/BookmarkControllerTest.kt @@ -3,6 +3,7 @@ package com.group4.ticketingservice.bookmark import com.google.gson.GsonBuilder import com.group4.ticketingservice.bookmark.BookmarkControllerTest.testFields.testUserId import com.group4.ticketingservice.bookmark.BookmarkControllerTest.testFields.testUserName +import com.group4.ticketingservice.config.GsonConfig import com.group4.ticketingservice.config.SecurityConfig import com.group4.ticketingservice.controller.BookmarkController import com.group4.ticketingservice.dto.BookmarkFromdto @@ -13,6 +14,7 @@ import com.group4.ticketingservice.filter.JwtAuthorizationEntryPoint import com.group4.ticketingservice.service.BookmarkService import com.group4.ticketingservice.user.WithAuthUser import com.group4.ticketingservice.utils.Authority +import com.group4.ticketingservice.utils.OffsetDateTimeAdapter import com.group4.ticketingservice.utils.TokenProvider import com.ninjasquad.springmockk.MockkBean import io.mockk.every @@ -24,11 +26,16 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.context.annotation.ComponentScan import org.springframework.context.annotation.FilterType +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Pageable import org.springframework.http.MediaType import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.ResultActions import org.springframework.test.web.servlet.request.MockMvcRequestBuilders +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath @@ -38,7 +45,7 @@ import java.time.OffsetDateTime @ExtendWith(MockKExtension::class) @WebMvcTest( controllers = [BookmarkController::class], - includeFilters = [ComponentScan.Filter(value = [(SecurityConfig::class), (JwtAuthorizationEntryPoint::class), (TokenProvider::class)], type = FilterType.ASSIGNABLE_TYPE)] + includeFilters = [ComponentScan.Filter(value = [(SecurityConfig::class), (JwtAuthorizationEntryPoint::class), (GsonConfig::class), (OffsetDateTimeAdapter::class), (TokenProvider::class)], type = FilterType.ASSIGNABLE_TYPE)] ) class BookmarkControllerTest( @Autowired val mockMvc: MockMvc @@ -78,11 +85,65 @@ class BookmarkControllerTest( event_id = 1 ) + val pageable: Pageable = PageRequest.of(0, 4) + val content = mutableListOf( + Bookmark( + id = 11, + user = sampleUser, + event = Event( + id = 1, + title = "정섭이의 코딩쇼", + date = OffsetDateTime.now(), + reservationEndTime = OffsetDateTime.now(), + reservationStartTime = OffsetDateTime.now(), + maxAttendees = 10 + ) + ), + Bookmark( + id = 12, + user = sampleUser, + event = Event( + id = 2, + title = "민준이의 전국군가잘함", + date = OffsetDateTime.now(), + reservationEndTime = OffsetDateTime.now(), + reservationStartTime = OffsetDateTime.now(), + maxAttendees = 10 + ) + ), + Bookmark( + id = 13, + user = sampleUser, + event = Event( + id = 3, + title = "하영이의 신작도서 팬싸인회", + date = OffsetDateTime.now(), + reservationEndTime = OffsetDateTime.now(), + reservationStartTime = OffsetDateTime.now(), + maxAttendees = 10 + ) + ), + Bookmark( + id = 14, + user = sampleUser, + event = Event( + id = 4, + title = "준하의 스파르타 코딩 동아리 설명회", + date = OffsetDateTime.now(), + reservationEndTime = OffsetDateTime.now(), + reservationStartTime = OffsetDateTime.now(), + maxAttendees = 10 + ) + ) + ) + val totalElements: Long = 100 + val page: Page = PageImpl(content, pageable, totalElements) + @Test @WithAuthUser(email = testUserName, id = testUserId) fun `POST_api_bookmark should invoke service_create`() { // given - every { service.create(testUserId, sampleBookmarkDto) } returns 1 + every { service.create(testUserId, sampleBookmarkDto) } returns sampleBookmark // when mockMvc.perform( @@ -99,7 +160,7 @@ class BookmarkControllerTest( @WithAuthUser(email = testUserName, id = testUserId) fun `POST_api_bookmark should return saved bookmark id with HTTP 201 Created`() { // given - every { service.create(testUserId, sampleBookmarkDto) } returns 1 + every { service.create(testUserId, sampleBookmarkDto) } returns sampleBookmark // when val resultActions: ResultActions = mockMvc.perform( @@ -110,14 +171,14 @@ class BookmarkControllerTest( // then resultActions.andExpect(status().isCreated) - .andExpect(content().json("1")) + .andExpect(jsonPath("$.data.id").value(sampleBookmark.id)) } @Test @WithAuthUser(email = testUserName, id = testUserId) fun `POST_api_bookmark should return HTTP ERROR 400 for invalid parameter`() { // given - every { service.create(testUserId, sampleBookmarkDto) } returns 1 + every { service.create(testUserId, sampleBookmarkDto) } returns sampleBookmark // when val resultActions: ResultActions = mockMvc.perform( @@ -133,70 +194,77 @@ class BookmarkControllerTest( @WithAuthUser(email = testUserName, id = testUserId) fun `GET_api_bookmarks should invoke service_getList`() { // given - every { service.getList(testUserId) } returns mutableListOf(sampleBookmark) + every { service.getBookmarks(testUserId, pageable) } returns page // when mockMvc.perform(MockMvcRequestBuilders.get("/bookmarks")) // then - verify(exactly = 1) { service.getList(testUserId) } + verify(exactly = 1) { service.getBookmarks(any(), any()) } } @Test @WithAuthUser(email = testUserName, id = testUserId) fun `GET_api_bookmarks should return list of bookmarks with HTTP 200 OK`() { // given - every { service.getList(testUserId) } returns mutableListOf(sampleBookmark) + every { service.getBookmarks(any(), any()) } returns page // when val resultActions: ResultActions = mockMvc.perform(MockMvcRequestBuilders.get("/bookmarks")) // then resultActions.andExpect(status().isOk) - .andExpect(jsonPath("$[0].id").value(1)) + .andExpect(jsonPath("$.data.[0].id").value(11)) } @Test @WithAuthUser(email = testUserName, id = testUserId) - fun `GET_api_bookmark should invoke service_get`() { + fun `GET_api_bookmarks should return page of bookmarks with pagination`() { // given - every { service.get(testUserId, 1) } returns sampleBookmark + every { service.getBookmarks(any(), any()) } returns page // when - mockMvc.perform(MockMvcRequestBuilders.get("/bookmarks/1")) + val result = mockMvc.perform( + get("/bookmarks") + .param("page", "1") + .param("size", "4") + ) // then - verify(exactly = 1) { service.get(testUserId, 1) } + result + .andExpect(status().isOk) + .andExpect(jsonPath("$.totalElements").value(totalElements)) + .andExpect(jsonPath("$.data.[0].id").value(11)) + .andExpect(jsonPath("$.data.[1].id").value(12)) } @Test @WithAuthUser(email = testUserName, id = testUserId) - fun `GET_api_bookmark should return found bookmark with HTTP 200 OK`() { + fun `GET_api_bookmark should invoke service_get`() { // given every { service.get(testUserId, 1) } returns sampleBookmark // when - val resultActions: ResultActions = mockMvc.perform(MockMvcRequestBuilders.get("/bookmarks/1")) + mockMvc.perform(MockMvcRequestBuilders.get("/bookmarks/1")) // then - resultActions.andExpect(status().isOk) - .andExpect(jsonPath("$.id").value(sampleBookmark.id)) - .andExpect(jsonPath("$.user.id").value(sampleBookmark.user.id)) - .andExpect(jsonPath("$.event.id").value(sampleBookmark.event.id)) + verify(exactly = 1) { service.get(testUserId, 1) } } @Test @WithAuthUser(email = testUserName, id = testUserId) - fun `GET_api_bookmark should return null with HTTP 200 OK if element is not found`() { + fun `GET_api_bookmark should return found bookmark with HTTP 200 OK`() { // given - every { service.get(testUserId, 1) } returns null + every { service.get(testUserId, 1) } returns sampleBookmark // when val resultActions: ResultActions = mockMvc.perform(MockMvcRequestBuilders.get("/bookmarks/1")) // then resultActions.andExpect(status().isOk) - .andExpect(content().string("null")) + .andExpect(jsonPath("$.data.id").value(sampleBookmark.id)) + .andExpect(jsonPath("$.data.user.id").value(sampleBookmark.user.id)) + .andExpect(jsonPath("$.data.event.id").value(sampleBookmark.event.id)) } @Test @@ -218,7 +286,7 @@ class BookmarkControllerTest( @Test @WithAuthUser(email = testUserName, id = testUserId) - fun `DELETE_api_bookmark_{bookmarkId} should return HTTP 204 No Content`() { + fun `DELETE_api_bookmark_{bookmarkId} should return HTTP 200 OK`() { // given every { service.delete(testUserId, 1) } returns Unit @@ -229,6 +297,6 @@ class BookmarkControllerTest( ) // then - resultActions.andExpect(status().isNoContent) + resultActions.andExpect(status().isOk) } } diff --git a/src/test/kotlin/com/group4/ticketingservice/bookmark/BookmarkServiceTest.kt b/src/test/kotlin/com/group4/ticketingservice/bookmark/BookmarkServiceTest.kt index 6c1fd8f9..1187ebf0 100644 --- a/src/test/kotlin/com/group4/ticketingservice/bookmark/BookmarkServiceTest.kt +++ b/src/test/kotlin/com/group4/ticketingservice/bookmark/BookmarkServiceTest.kt @@ -12,8 +12,13 @@ import com.group4.ticketingservice.utils.Authority import io.mockk.every import io.mockk.mockk import io.mockk.verify +import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.modelmapper.ModelMapper +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Pageable import java.time.OffsetDateTime class BookmarkServiceTest() { @@ -50,30 +55,99 @@ class BookmarkServiceTest() { event_id = 1 ) + val pageable: Pageable = PageRequest.of(0, 4) + val content = mutableListOf( + Bookmark( + id = 11, + user = sampleUser, + event = Event( + id = 1, + title = "정섭이의 코딩쇼", + date = OffsetDateTime.now(), + reservationEndTime = OffsetDateTime.now(), + reservationStartTime = OffsetDateTime.now(), + maxAttendees = 10 + ) + ), + Bookmark( + id = 12, + user = sampleUser, + event = Event( + id = 2, + title = "민준이의 전국군가잘함", + date = OffsetDateTime.now(), + reservationEndTime = OffsetDateTime.now(), + reservationStartTime = OffsetDateTime.now(), + maxAttendees = 10 + ) + ), + Bookmark( + id = 13, + user = sampleUser, + event = Event( + id = 3, + title = "하영이의 신작도서 팬싸인회", + date = OffsetDateTime.now(), + reservationEndTime = OffsetDateTime.now(), + reservationStartTime = OffsetDateTime.now(), + maxAttendees = 10 + ) + ), + Bookmark( + id = 14, + user = sampleUser, + event = Event( + id = 4, + title = "준하의 스파르타 코딩 동아리 설명회", + date = OffsetDateTime.now(), + reservationEndTime = OffsetDateTime.now(), + reservationStartTime = OffsetDateTime.now(), + maxAttendees = 10 + ) + ) + ) + val totalElements: Long = 100 + val page: Page = PageImpl(content, pageable, totalElements) + @Test fun `bookmarkService_getList() invoke repository_findByUser`() { // given - every { repository.findByUserId(sampleUserId) } returns listOf(sampleBookmark) + every { repository.findByUserId(sampleUserId, pageable) } returns page + + // when + bookmarkService.getBookmarks(sampleUserId, pageable) + + // then + verify(exactly = 1) { repository.findByUserId(sampleUserId, pageable) } + } + + @Test + fun `bookmarkService_getBookmarks() return page with pagination`() { + // given + every { repository.findByUserId(sampleUserId, pageable) } returns page // when - bookmarkService.getList(sampleUserId) + val result: Page = bookmarkService.getBookmarks(sampleUserId, pageable) // then - verify(exactly = 1) { repository.findByUserId(sampleUserId) } + assertThat(result.totalElements).isEqualTo(totalElements) + assertThat(result.numberOfElements).isEqualTo(content.size) + assertThat(result.content[0].id).isEqualTo(content[0].id) + assertThat(result.content[1].id).isEqualTo(content[1].id) } @Test fun `bookmarkService_getList() should return emptyList`() { // given - every { repository.findByUserId(sampleUserId) } returns listOf() + every { repository.findByUserId(sampleUserId, pageable) } returns page // when - val result: List = bookmarkService.getList(sampleUserId) + val result = bookmarkService.getBookmarks(sampleUserId, pageable) // then - verify(exactly = 1) { repository.findByUserId(sampleUserId) } - assert(result == listOf()) + verify(exactly = 1) { repository.findByUserId(sampleUserId, pageable) } + assert(result == page) } @Test diff --git a/src/test/kotlin/com/group4/ticketingservice/event/EventControllerTest.kt b/src/test/kotlin/com/group4/ticketingservice/event/EventControllerTest.kt index a42912b2..1d8e0253 100644 --- a/src/test/kotlin/com/group4/ticketingservice/event/EventControllerTest.kt +++ b/src/test/kotlin/com/group4/ticketingservice/event/EventControllerTest.kt @@ -17,6 +17,10 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.context.annotation.ComponentScan import org.springframework.context.annotation.FilterType +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Pageable import org.springframework.http.MediaType import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get @@ -63,6 +67,45 @@ class EventControllerTest( ) + val pageable: Pageable = PageRequest.of(0, 4) + val content = mutableListOf( + Event( + id = 2, + title = "민준이의 전국군가잘함", + date = OffsetDateTime.now(), + reservationEndTime = OffsetDateTime.now(), + reservationStartTime = OffsetDateTime.now(), + maxAttendees = 10 + ), + Event( + id = 1, + title = "정섭이의 코딩쇼", + date = OffsetDateTime.now(), + reservationEndTime = OffsetDateTime.now(), + reservationStartTime = OffsetDateTime.now(), + maxAttendees = 10 + ), + Event( + id = 4, + title = "준하의 스파르타 코딩 동아리 설명회", + date = OffsetDateTime.now(), + reservationEndTime = OffsetDateTime.now(), + reservationStartTime = OffsetDateTime.now(), + maxAttendees = 10 + ), + Event( + id = 3, + title = "하영이의 신작도서 팬싸인회", + date = OffsetDateTime.now(), + reservationEndTime = OffsetDateTime.now(), + reservationStartTime = OffsetDateTime.now(), + maxAttendees = 10 + ) + ) + val totalElements: Long = 100 + val page: Page = PageImpl(content, pageable, totalElements) + val emptyPage: Page = PageImpl(listOf(), pageable, listOf().size.toLong()) + @Test fun `POST events should return created event`() { every { eventService.createEvent(any(), any(), any(), any(), any()) } returns sampleEvent @@ -78,11 +121,11 @@ class EventControllerTest( .contentType(MediaType.APPLICATION_JSON) .content(eventCreateRequest) ) - .andExpect(status().isOk) + .andExpect(status().isCreated) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$.id").value(sampleEvent.id)) - .andExpect(jsonPath("$.title").value(sampleEvent.title)) - .andExpect(jsonPath("$.maxAttendees").value(sampleEvent.maxAttendees)) + .andExpect(jsonPath("$.data.id").value(sampleEvent.id)) + .andExpect(jsonPath("$.data.title").value(sampleEvent.title)) + .andExpect(jsonPath("$.data.maxAttendees").value(sampleEvent.maxAttendees)) } @Test @@ -94,7 +137,7 @@ class EventControllerTest( ) .andExpect(status().isOk) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$.id").value(sampleEvent.id)) + .andExpect(jsonPath("$.data.id").value(sampleEvent.id)) } @Test @@ -109,25 +152,52 @@ class EventControllerTest( @Test fun `GET List of events should return list of events`() { - every { eventService.getEvents() } returns listOf(sampleEvent) - mockMvc.perform( - get("/events/") + // Given + every { eventService.getEvents(any(), any()) } returns page + + // When + val result = mockMvc.perform( + get("/events") .contentType(MediaType.APPLICATION_JSON) ) + + // Then + result .andExpect(status().isOk) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$[0].id").value(sampleEvent.id)) + .andExpect(jsonPath("$.data.[0].id").value(content[0].id)) + } + + @Test + fun `GET List of events should return list of events with pagination and sorting`() { + // Given + every { eventService.getEvents(any(), any()) } returns page + + // When + val result = mockMvc.perform( + get("/events") + .param("page", "1") + .param("size", "4") + .param("sort", "title") + ) + + // Then + result + .andExpect(status().isOk) + .andExpect(jsonPath("$.totalElements").value(totalElements)) + .andExpect(jsonPath("$.data.[0].id").value(2)) + .andExpect(jsonPath("$.data.[1].id").value(1)) } @Test fun `GET List of events should return empty list`() { - every { eventService.getEvents() } returns listOf() + every { eventService.getEvents(any(), any()) } returns emptyPage mockMvc.perform( - get("/events/") + get("/events") .contentType(MediaType.APPLICATION_JSON) ) - .andExpect(status().isNoContent) + .andExpect(status().isOk) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$").isEmpty) + .andExpect(jsonPath("$.data").isEmpty) } } diff --git a/src/test/kotlin/com/group4/ticketingservice/event/EventServiceTest.kt b/src/test/kotlin/com/group4/ticketingservice/event/EventServiceTest.kt index b4ad61e4..b16fef84 100644 --- a/src/test/kotlin/com/group4/ticketingservice/event/EventServiceTest.kt +++ b/src/test/kotlin/com/group4/ticketingservice/event/EventServiceTest.kt @@ -1,5 +1,6 @@ package com.group4.ticketingservice.event +import com.group4.ticketingservice.dto.EventSpecifications import com.group4.ticketingservice.entity.Event import com.group4.ticketingservice.entity.User import com.group4.ticketingservice.repository.EventRepository @@ -9,7 +10,13 @@ import com.group4.ticketingservice.utils.Authority import io.mockk.every import io.mockk.mockk import io.mockk.verify +import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test +import org.springframework.data.domain.Page +import org.springframework.data.domain.PageImpl +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Pageable +import org.springframework.data.domain.Sort import java.time.OffsetDateTime import java.util.Optional @@ -34,8 +41,49 @@ class EventServiceTest { reservationEndTime = OffsetDateTime.now(), reservationStartTime = OffsetDateTime.now(), maxAttendees = 10 + ) + val pageable: Pageable = PageRequest.of(0, 4, Sort.by("title").ascending()) + val content = mutableListOf( + Event( + id = 2, + title = "민준이의 전국군가잘함", + date = OffsetDateTime.now(), + reservationEndTime = OffsetDateTime.now(), + reservationStartTime = OffsetDateTime.now(), + maxAttendees = 10 + ), + Event( + id = 1, + title = "정섭이의 코딩쇼", + date = OffsetDateTime.now(), + reservationEndTime = OffsetDateTime.now(), + reservationStartTime = OffsetDateTime.now(), + maxAttendees = 10 + ), + Event( + id = 4, + title = "준하의 스파르타 코딩 동아리 설명회", + date = OffsetDateTime.now(), + reservationEndTime = OffsetDateTime.now(), + reservationStartTime = OffsetDateTime.now(), + maxAttendees = 10 + ), + Event( + id = 3, + title = "하영이의 신작도서 팬싸인회", + date = OffsetDateTime.now(), + reservationEndTime = OffsetDateTime.now(), + reservationStartTime = OffsetDateTime.now(), + maxAttendees = 10 + ) ) + val totalElements: Long = 100 + val page: Page = PageImpl(content, pageable, totalElements) + val emptyPage: Page = PageImpl(listOf(), pageable, listOf().size.toLong()) + + val title = "코딩" + val specification = EventSpecifications.withTitle(title) @Test fun `EventService_createEvent invoke EventRepository_findById`() { @@ -60,8 +108,23 @@ class EventServiceTest { @Test fun `EventService_getEvents invoke EventRepository_findAll`() { - every { eventRepository.findAll() } returns listOf(sampleEvent) - eventService.getEvents() - verify(exactly = 1) { eventRepository.findAll() } + every { eventRepository.findAll(any(), pageable) } returns page + eventService.getEvents(title, pageable) + verify(exactly = 1) { eventRepository.findAll(any(), pageable) } + } + + @Test + fun `EventService_getEvents return page with pagination and sorting`() { + // Given + every { eventRepository.findAll(any(), pageable) } returns page + + // When + val result: Page = eventService.getEvents(null, pageable) + + // Then + assertThat(result.totalElements).isEqualTo(totalElements) + assertThat(result.numberOfElements).isEqualTo(content.size) + assertThat(result.content[0].id).isEqualTo(content[0].id) + assertThat(result.content[1].id).isEqualTo(content[1].id) } } diff --git a/src/test/kotlin/com/group4/ticketingservice/filter/JwtAuthenticationFilterTest.kt b/src/test/kotlin/com/group4/ticketingservice/filter/JwtAuthenticationFilterTest.kt index 2f1d8db2..87622c9d 100644 --- a/src/test/kotlin/com/group4/ticketingservice/filter/JwtAuthenticationFilterTest.kt +++ b/src/test/kotlin/com/group4/ticketingservice/filter/JwtAuthenticationFilterTest.kt @@ -92,28 +92,6 @@ class JwtAuthenticationFilterTest { verify(exactly = 1) { tokenProvider.createToken(any()) } } - @Test - fun `JwtAuthenticationFilter_dofilter() should write message at JwtAuthenticationFilter_unsuccessfulAuthentication when credential is bad`() { - // given - - every { authenticationManager.authenticate(any()) } throws BadCredentialsException("") - - // when - val req = MockHttpServletRequest("POST", "/login") - req.servletPath = "/login" - val res = MockHttpServletResponse() - val chain = MockFilterChain() - - val requestJson = ObjectMapper().writeValueAsString(sampleSignInRequest) - req.setContent(requestJson.toByteArray()) - - filter.doFilter(req, res, chain) - - // then - - assertThat(String(res.contentAsByteArray).contains("error_code")).isTrue() - } - @Test fun `JwtAuthenticationFilter_attemptAuthentication() should throw exception when credential is bad `() { // given diff --git a/src/test/kotlin/com/group4/ticketingservice/reservation/ReservationControllerTest.kt b/src/test/kotlin/com/group4/ticketingservice/reservation/ReservationControllerTest.kt index 13ccfac4..e83f0c23 100644 --- a/src/test/kotlin/com/group4/ticketingservice/reservation/ReservationControllerTest.kt +++ b/src/test/kotlin/com/group4/ticketingservice/reservation/ReservationControllerTest.kt @@ -1,12 +1,18 @@ package com.group4.ticketingservice.reservation +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.KotlinModule import com.google.gson.Gson import com.google.gson.GsonBuilder +import com.group4.ticketingservice.config.GsonConfig import com.group4.ticketingservice.config.SecurityConfig import com.group4.ticketingservice.controller.ReservationController import com.group4.ticketingservice.dto.ReservationCreateRequest import com.group4.ticketingservice.dto.ReservationDeleteRequest +import com.group4.ticketingservice.dto.ReservationResponse import com.group4.ticketingservice.dto.ReservationUpdateRequest +import com.group4.ticketingservice.dto.SuccessResponseDTO import com.group4.ticketingservice.entity.Event import com.group4.ticketingservice.entity.Reservation import com.group4.ticketingservice.entity.User @@ -16,14 +22,17 @@ import com.group4.ticketingservice.reservation.ReservationControllerTest.testFie import com.group4.ticketingservice.service.ReservationService import com.group4.ticketingservice.user.WithAuthUser import com.group4.ticketingservice.utils.Authority +import com.group4.ticketingservice.utils.OffsetDateTimeAdapter import com.group4.ticketingservice.utils.TokenProvider import com.ninjasquad.springmockk.MockkBean import io.mockk.every +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.context.annotation.ComponentScan import org.springframework.context.annotation.FilterType +import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete @@ -38,7 +47,7 @@ import java.time.OffsetDateTime @WebMvcTest( ReservationController::class, includeFilters = arrayOf( - ComponentScan.Filter(value = [ (SecurityConfig::class), (TokenProvider::class), (JwtAuthorizationEntryPoint::class)], type = FilterType.ASSIGNABLE_TYPE) + ComponentScan.Filter(value = [ (SecurityConfig::class), (TokenProvider::class), (GsonConfig::class), (OffsetDateTimeAdapter::class), (JwtAuthorizationEntryPoint::class)], type = FilterType.ASSIGNABLE_TYPE) ) ) @@ -91,16 +100,22 @@ class ReservationControllerTest( fun `POST reservations should return created reservation`() { every { reservationService.createReservation(1, 1) } returns sampleReservation - mockMvc.perform( + val result = mockMvc.perform( post("/reservations") .contentType(MediaType.APPLICATION_JSON) .content(Gson().toJson(sampleReservationCreateRequest)) - ) - .andExpect(status().isOk) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$.id").value(sampleReservation.id)) - .andExpect(jsonPath("$.userId").value(sampleReservation.user.id)) - .andExpect(jsonPath("$.eventId").value(sampleReservation.event.id)) + ).andReturn() + + val response = result.response + val objectMapper = ObjectMapper().registerModule(JavaTimeModule()).registerModule(KotlinModule()) + val successResponseDTO = objectMapper.readValue(response.contentAsString, SuccessResponseDTO::class.java) + val data = objectMapper.convertValue(successResponseDTO.data, ReservationResponse::class.java) + + assertEquals(HttpStatus.CREATED.value(), result.response.status) + assertEquals(MediaType.APPLICATION_JSON.toString(), result.response.contentType) + assertEquals(sampleReservation.id, data.id) + assertEquals(sampleReservation.user.id, data.userId) + assertEquals(sampleReservation.event.id, data.eventId) } @Test @@ -114,9 +129,9 @@ class ReservationControllerTest( ) .andExpect(status().isOk) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$.id").value(sampleReservation.id)) - .andExpect(jsonPath("$.userId").value(sampleReservation.user.id)) - .andExpect(jsonPath("$.eventId").value(sampleReservation.event.id)) + .andExpect(jsonPath("$.data.id").value(sampleReservation.id)) + .andExpect(jsonPath("$.data.userId").value(sampleReservation.user.id)) + .andExpect(jsonPath("$.data.eventId").value(sampleReservation.event.id)) } @Test @@ -147,9 +162,9 @@ class ReservationControllerTest( ) .andExpect(status().isOk) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) - .andExpect(jsonPath("$.id").value(updatedReservation.id)) - .andExpect(jsonPath("$.userId").value(updatedReservation.user.id)) - .andExpect(jsonPath("$.eventId").value(updatedReservation.event.id)) + .andExpect(jsonPath("$.data.id").value(updatedReservation.id)) + .andExpect(jsonPath("$.data.userId").value(updatedReservation.user.id)) + .andExpect(jsonPath("$.data.eventId").value(updatedReservation.event.id)) } @Test @@ -161,6 +176,6 @@ class ReservationControllerTest( delete("/reservations/${sampleReservationDeleteRequest.id}") .contentType(MediaType.APPLICATION_JSON) ) - .andExpect(status().isNoContent) + .andExpect(status().isOk) } } diff --git a/src/test/kotlin/com/group4/ticketingservice/user/UserControllerTest.kt b/src/test/kotlin/com/group4/ticketingservice/user/UserControllerTest.kt index b9006d05..594c179f 100644 --- a/src/test/kotlin/com/group4/ticketingservice/user/UserControllerTest.kt +++ b/src/test/kotlin/com/group4/ticketingservice/user/UserControllerTest.kt @@ -77,8 +77,8 @@ class UserControllerTest( val resultActions: ResultActions = mockMvc.perform(MockMvcRequestBuilders.get("/users/access_token_info")) resultActions.andExpect(MockMvcResultMatchers.status().isOk) - .andExpect(MockMvcResultMatchers.jsonPath("$.expires_in").exists()) - .andExpect(MockMvcResultMatchers.jsonPath("$.userId").value(testUserId)) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.expires_in").exists()) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.userId").value(testUserId)) } @Test @@ -114,7 +114,7 @@ class UserControllerTest( // then resultActions.andExpect(MockMvcResultMatchers.status().isCreated) - .andExpect(MockMvcResultMatchers.jsonPath("$.email").value(testUserName)) + .andExpect(MockMvcResultMatchers.jsonPath("$.data.email").value(testUserName)) } @Test diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index 5fe017c3..4f60a1ac 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -8,3 +8,4 @@ ticketing.jwt.secret=d2VhcmVuYXZ5c3dkZXZlbG9wZXJzLmFuZGlhbW1pbmp1bjMwMjE= ticketing.jwt.expiration-hours=24 ticketing.jwt.issuer=minjun +spring.data.web.pageable.one-indexed-parameters=true \ No newline at end of file