Skip to content

Commit cb39c03

Browse files
Feat/23 implement Bookmark (#28)
* feat: init bookmark rest api * test: add create BookmarkController test * test: add BookmarkControllerTest (TDD) * feat: implement Controller for test success * test: add BookmarkServiceTest (TDD) * refactor: refactor BookmarkService * test: add unit test for BookmarkService * refactor: change directory to entity from model * test: fix test 'bookmarkService_getList() should return emptyList' * test: add BookmarkRepositoryTest (TDD) * test: change from using SpringBootTest to using DataJpaTest to use h2 Database * test: add Given-When-Then comment and change Response HTTP Status Code * refactor: refactor BookmarkController - change variable names - change response HTTP status - add Content-Location * refactor: change endpoints to comply with the REST API Design Guide * test: separate into state verification and behavior verification and add not found test * fix: fix getBookmark(id) in case element not found * refactor: unify the type for the variable "id" in the same domain
1 parent 1ba3b3a commit cb39c03

File tree

11 files changed

+498
-4
lines changed

11 files changed

+498
-4
lines changed

build.gradle.kts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,16 @@ repositories {
2424
}
2525

2626
dependencies {
27-
// implementation("org.springframework.boot:spring-boot-starter-data-jpa")
27+
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
2828
implementation("org.springframework.boot:spring-boot-starter-web")
2929
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
3030
implementation("org.jetbrains.kotlin:kotlin-reflect")
31+
implementation("com.ninja-squad:springmockk:4.0.2")
3132
developmentOnly("org.springframework.boot:spring-boot-devtools")
32-
// runtimeOnly("com.mysql:mysql-connector-j")
33+
runtimeOnly("com.mysql:mysql-connector-j")
3334
testImplementation("org.springframework.boot:spring-boot-starter-test")
34-
// implementation("org.modelmapper:modelmapper:2.4.2")
35+
testImplementation("com.h2database:h2")
36+
implementation("org.modelmapper:modelmapper:2.4.2")
3537
}
3638

3739
tasks.withType<KotlinCompile> {
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.group4.ticketingservice.config
2+
3+
import org.modelmapper.ModelMapper
4+
import org.springframework.context.annotation.Bean
5+
import org.springframework.context.annotation.Configuration
6+
7+
@Configuration
8+
class Config {
9+
10+
@Bean
11+
fun modelMapper(): ModelMapper {
12+
val modelMapper = ModelMapper()
13+
modelMapper.configuration.isFieldMatchingEnabled = true
14+
return modelMapper
15+
}
16+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package com.group4.ticketingservice.controller
2+
3+
import com.group4.ticketingservice.dto.BookmarkFromdto
4+
import com.group4.ticketingservice.service.BookmarkService
5+
import org.springframework.beans.factory.annotation.Autowired
6+
import org.springframework.http.HttpHeaders
7+
import org.springframework.http.HttpStatus
8+
import org.springframework.http.ResponseEntity
9+
import org.springframework.web.bind.MethodArgumentNotValidException
10+
import org.springframework.web.bind.annotation.DeleteMapping
11+
import org.springframework.web.bind.annotation.GetMapping
12+
import org.springframework.web.bind.annotation.PathVariable
13+
import org.springframework.web.bind.annotation.PostMapping
14+
import org.springframework.web.bind.annotation.RequestMapping
15+
import org.springframework.web.bind.annotation.RestController
16+
17+
@RestController // REST API
18+
@RequestMapping("bookmarks")
19+
class BookmarkController @Autowired constructor(val bookmarkService: BookmarkService) {
20+
21+
// 북마크 등록
22+
@PostMapping
23+
fun addBookmark(boardFormDto: BookmarkFromdto): ResponseEntity<Any> {
24+
val savedBookmarkId = bookmarkService.create(boardFormDto)
25+
val headers = HttpHeaders()
26+
headers.set("Content-Location", "/bookmark/%d".format(savedBookmarkId))
27+
return ResponseEntity.status(HttpStatus.CREATED).headers(headers).body(savedBookmarkId)
28+
}
29+
30+
// 특정 북마크 조회하기
31+
@GetMapping("/{id}")
32+
fun getBookmark(@PathVariable id: Int): ResponseEntity<out Any?> {
33+
try {
34+
val foundBookmark = bookmarkService.get(id)
35+
return ResponseEntity.status(HttpStatus.OK).body(foundBookmark ?: "null")
36+
} catch (e: MethodArgumentNotValidException) {
37+
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build()
38+
}
39+
}
40+
41+
// 북마크 삭제
42+
@DeleteMapping("/{id}")
43+
fun deleteBookmark(@PathVariable id: Int): ResponseEntity<Any> {
44+
bookmarkService.delete(id)
45+
return ResponseEntity.status(HttpStatus.NO_CONTENT).build()
46+
}
47+
48+
// 전체사용자 북마크 목록
49+
@GetMapping()
50+
fun getBookmarks(): ResponseEntity<Any> {
51+
return ResponseEntity.status(HttpStatus.OK).body(bookmarkService.getList())
52+
}
53+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.group4.ticketingservice.dto
2+
3+
data class BookmarkFromdto(
4+
var user_id: Int,
5+
var show_id: Int
6+
)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.group4.ticketingservice.entity
2+
3+
import jakarta.persistence.Entity
4+
import jakarta.persistence.GeneratedValue
5+
import jakarta.persistence.GenerationType
6+
import jakarta.persistence.Id
7+
8+
@Entity
9+
class Bookmark(
10+
@Id
11+
@GeneratedValue(strategy = GenerationType.IDENTITY)
12+
var id: Int? = null,
13+
var user_id: Int, // 사용자 식별자
14+
var show_id: Int // 공연 식별자
15+
)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.group4.ticketingservice.repository
2+
3+
import com.group4.ticketingservice.entity.Bookmark
4+
import org.springframework.data.jpa.repository.JpaRepository
5+
import org.springframework.stereotype.Repository
6+
7+
@Repository
8+
interface BookmarkRepository : JpaRepository<Bookmark, Long>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.group4.ticketingservice.service
2+
3+
import com.group4.ticketingservice.dto.BookmarkFromdto
4+
import com.group4.ticketingservice.entity.Bookmark
5+
import com.group4.ticketingservice.repository.BookmarkRepository
6+
import org.modelmapper.ModelMapper
7+
import org.springframework.beans.factory.annotation.Autowired
8+
import org.springframework.data.repository.findByIdOrNull
9+
import org.springframework.stereotype.Service
10+
11+
@Service
12+
class BookmarkService @Autowired constructor(
13+
val bookmarkRepository: BookmarkRepository,
14+
val modelMapper: ModelMapper
15+
) {
16+
17+
fun create(bookmarkFormDto: BookmarkFromdto): Int? {
18+
return bookmarkRepository.save(modelMapper.map(bookmarkFormDto, Bookmark::class.java)).id
19+
}
20+
21+
fun get(id: Int): Bookmark? {
22+
return bookmarkRepository.findByIdOrNull(id.toLong())
23+
}
24+
25+
fun delete(id: Int) {
26+
bookmarkRepository.deleteById(id.toLong())
27+
}
28+
fun getList(): List<Bookmark> {
29+
return bookmarkRepository.findAll()
30+
}
31+
}

src/test/kotlin/com/group4/ticketingservice/TicketingserviceApplicationTests.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import org.springframework.boot.test.context.SpringBootTest
77
class TicketingserviceApplicationTests {
88

99
@Test
10-
fun contextLoads() {
10+
fun contextLoads(): Boolean {
11+
return true
1112
}
1213
}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
package com.group4.ticketingservice.bookmark
2+
3+
import com.group4.ticketingservice.controller.BookmarkController
4+
import com.group4.ticketingservice.dto.BookmarkFromdto
5+
import com.group4.ticketingservice.entity.Bookmark
6+
import com.group4.ticketingservice.service.BookmarkService
7+
import com.ninjasquad.springmockk.MockkBean
8+
import io.mockk.every
9+
import io.mockk.verify
10+
import org.junit.jupiter.api.Test
11+
import org.springframework.beans.factory.annotation.Autowired
12+
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
13+
import org.springframework.http.MediaType
14+
import org.springframework.test.web.servlet.MockMvc
15+
import org.springframework.test.web.servlet.ResultActions
16+
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
17+
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
18+
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
19+
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
20+
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
21+
22+
@WebMvcTest(BookmarkController::class)
23+
class BookmarkControllerTest(@Autowired val mockMvc: MockMvc) {
24+
@MockkBean
25+
private lateinit var service: BookmarkService
26+
private val sampleBookmark = Bookmark(
27+
user_id = 1,
28+
show_id = 1
29+
)
30+
private val sampleBookmarkDto = BookmarkFromdto(
31+
user_id = 1,
32+
show_id = 1
33+
)
34+
35+
@Test
36+
fun `POST_api_bookmark should invoke service_create`() {
37+
// given
38+
every { service.create(sampleBookmarkDto) } returns 1
39+
40+
// when
41+
mockMvc.perform(
42+
post("/bookmarks")
43+
.contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
44+
.param("user_id", sampleBookmark.user_id.toString())
45+
.param("show_id", sampleBookmark.show_id.toString())
46+
)
47+
48+
// then
49+
verify(exactly = 1) { service.create(sampleBookmarkDto) }
50+
}
51+
52+
@Test
53+
fun `POST_api_bookmark should return saved bookmark id with HTTP 201 Created`() {
54+
// given
55+
every { service.create(sampleBookmarkDto) } returns 1
56+
57+
// when
58+
val resultActions: ResultActions = mockMvc.perform(
59+
post("/bookmarks")
60+
.contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
61+
.param("user_id", sampleBookmark.user_id.toString())
62+
.param("show_id", sampleBookmark.show_id.toString())
63+
)
64+
65+
// then
66+
resultActions.andExpect(status().isCreated)
67+
.andExpect(content().json("1"))
68+
}
69+
70+
@Test
71+
fun `POST_api_bookmark should return HTTP ERROR 400 for invalid parameter`() {
72+
// given
73+
every { service.create(sampleBookmarkDto) } returns 1
74+
75+
// when
76+
val resultActions: ResultActions = mockMvc.perform(
77+
post("/bookmarks")
78+
.contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
79+
.param("user_id", sampleBookmark.user_id.toString())
80+
.param("show_id", sampleBookmark.show_id.toString())
81+
)
82+
83+
// then
84+
resultActions.andExpect(status().isCreated)
85+
.andExpect(content().json("1"))
86+
}
87+
88+
@Test
89+
fun `GET_api_bookmarks should invoke service_getList`() {
90+
// given
91+
every { service.getList() } returns mutableListOf(sampleBookmark)
92+
93+
// when
94+
mockMvc.perform(MockMvcRequestBuilders.get("/bookmarks"))
95+
96+
// then
97+
verify(exactly = 1) { service.getList() }
98+
}
99+
100+
@Test
101+
fun `GET_api_bookmarks should return list of bookmarks with HTTP 200 OK`() {
102+
// given
103+
every { service.getList() } returns mutableListOf(sampleBookmark)
104+
105+
// when
106+
val resultActions: ResultActions = mockMvc.perform(MockMvcRequestBuilders.get("/bookmarks"))
107+
108+
// then
109+
resultActions.andExpect(status().isOk)
110+
.andExpect(jsonPath("$[0].user_id").value(sampleBookmark.user_id))
111+
}
112+
113+
@Test
114+
fun `GET_api_bookmark should invoke service_get`() {
115+
// given
116+
every { service.get(1) } returns sampleBookmark
117+
118+
// when
119+
mockMvc.perform(MockMvcRequestBuilders.get("/bookmarks/1"))
120+
121+
// then
122+
verify(exactly = 1) { service.get(1) }
123+
}
124+
125+
@Test
126+
fun `GET_api_bookmark should return found bookmark with HTTP 200 OK`() {
127+
// given
128+
every { service.get(1) } returns sampleBookmark
129+
130+
// when
131+
val resultActions: ResultActions = mockMvc.perform(MockMvcRequestBuilders.get("/bookmarks/1"))
132+
133+
// then
134+
resultActions.andExpect(status().isOk)
135+
.andExpect(jsonPath("$.id").value(sampleBookmark.id))
136+
.andExpect(jsonPath("$.user_id").value(sampleBookmark.user_id))
137+
.andExpect(jsonPath("$.show_id").value(sampleBookmark.show_id))
138+
}
139+
140+
@Test
141+
fun `GET_api_bookmark should return null with HTTP 200 OK if element is not found`() {
142+
// given
143+
every { service.get(1) } returns null
144+
145+
// when
146+
val resultActions: ResultActions = mockMvc.perform(MockMvcRequestBuilders.get("/bookmarks/1"))
147+
148+
// then
149+
resultActions.andExpect(status().isOk)
150+
.andExpect(content().string("null"))
151+
}
152+
153+
@Test
154+
fun `DELETE_api_bookmark_{bookmarkId} should invoke service_delete`() {
155+
// given
156+
every { service.delete(1) } returns Unit
157+
158+
// when
159+
mockMvc.perform(
160+
MockMvcRequestBuilders
161+
.delete("/bookmarks/1")
162+
)
163+
164+
// then
165+
verify(exactly = 1) { service.delete(1) }
166+
}
167+
168+
@Test
169+
fun `DELETE_api_bookmark_{bookmarkId} should return HTTP 204 No Content`() {
170+
// given
171+
every { service.delete(1) } returns Unit
172+
173+
// when
174+
val resultActions: ResultActions = mockMvc.perform(
175+
MockMvcRequestBuilders
176+
.delete("/bookmarks/1")
177+
)
178+
179+
// then
180+
resultActions.andExpect(status().isNoContent)
181+
}
182+
}

0 commit comments

Comments
 (0)