Kotlinでアプリを作成してみた【REAL Webapp】

## spring.initializr でプロジェクトを作成する

(1) ブラウザを開き、(https://start.spring.io/) を入力し、開く
(2) 下記を入力し、Dependenciesで下記を選択し、『GENERATE』ボタンを押す

- Project: Maven
- Language: kotlin
- SpringBoot: 3.3.6

- Group: com.devtiro
- Artifact: bookstore
- Name : bookstore
- Description: Kotlin Course Bookstore
- Package name: com devtiro bookstore

Packageing: jar
Java: 21

Dependencies
- H2 Database
- Spring Data JPA
- Spring Web

## Domain Classes

①domain/entities/AuthorEntity.ktを作成する

```domain/entities/AuthorEntity.kt
package com.devtiro.bookstore.domain

class Author (val id: Long?, val name: String, val age: Int, val description: String, val image: String)
```

②domain/entities/BookEntity.ktを作成する

```domain/entities/BookEntity.kt
package com.devtiro.bookstore.domain

class Book (var isbn: String, var title: String, var description: String, var image: String, var author: Author)
```

## A Real Database

①application.propertiesを編集する

```application.properties
spring.application.name=bookstore

spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/postgresql
spring.datasource.username=postgresql
spring.datasource.password=root
```

## Entities

①domain/entities/AuthorEntity.ktを編集する

```domain/entities/AuthorEntity.kt
package com.devtiro.bookstore.domain

import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.Id
import jakarta.persistence.Table

@Entity
@Table(name="author")
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "author_id_seq")
data class Author (
    @Id
    @Column(name="id")
    val id: Long?,

    @Column(name="name")
    val name: String,

    @Column(name="age")
    val age: Int,

    @Column(name="description")
    val description: String,

    @Column(name="image")
    val image: String)
```

②domain/entities/BookEntity.ktを編集する

```domain/entities/BookEntity.kt
package com.devtiro.bookstore.domain

import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.Table

@Entity
@Table(name="books")
data class Book (
    @Id
    @Column(name="isbn")
    var isbn: String,

    @Column(name="title")
    var title: String,

    @Column(name="description")
    var description: String,

    @Column(name="image")
    var image: String,

    @ManyToOne(cascade=[CascadeType.DETACH])
    @JoinColumn(name="author_id")
    var author: Author
    )
```

## Repositories

①repositories/AuthorRepositories.ktを作成する

```domain/AuthorRepository.kt
package com.devtiro.bookstore.repositories

import com.devtiro.bookstore.domain.Author
import org.springframework.data.jpa.repositories.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface AuthorRepository : JpaRepository<Author, Long?>{
}
```

②repositories/BookRepository.ktを作成する

```domain/Book.kt
package com.devtiro.bookstore.repositories

import com.devtiro.bookstore.domain.Author
import org.springframework.data.jpa.repositories.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface BookRepository : JpaRepository<Book, Long?>{
}
```
③resouces/db.migration/V1_init_db.sqlを作成する

```resouces/db.migration/V1_init_db.sql
DROP SEQUENCE IF EXISTS "author_id seq";
CREATE SEQUENCE "author_id seq" INCREMENT BY 50 START WITH 1;

DROP SEQUENCE IF EXISTS "authors";
CREATE TABLE "authors" (
    "id" bigint NOT NULL,
    "age" smallint,
    "description" VARCHAR(512),
    "image" VARCHAR(512),
    "name" VARCHAR(512),
    CONSTRAINT "authors_pkey" PRIMARY KEY("id")
);

DROP SEQUENCE IF EXISTS "books";
CREATE TABLE "books" (
    "isbn" VARCHAR(19) NOT NULL,
    "description" VARCHAR(2048),
    "image" VARCHAR(512),
    "title" VARCHAR(512),
    "author_id" bigint,
    CONSTRAINT "books_pkey" PRIMARY KEY("isbn")
);
```

## REST API Theory

①controllers/AuthorsController.ktを作成する

```controllers/AuthorsController.kt
package com.devtiro.bookstore.controllers

import org.springframework.web.bind.annoatation.PostMapping
import org.springframework.web.bind.annoatation.RequestBody
import org.springframework.web.bind.annoatation.RestController

@RestController
class AuthorController {

    @PostMapping(path = ["/v1/authors"])
    fun createAuthor(@RequestBody AuthorEntity: AuthorDto): AuthorDto{
    }
}
```

## Creating DTOs

①domain/dto/AuthorDto.ktを作成する

```domain/dto/AuthorDto.kt
package com.devtiro.bookstore.domain.dto

data class AuthorDto(
    val id: Long?,
    val name: String,
    val age: Int,
    val description: String,
    val image: String
    )
```

②domain/dto/BookDto.ktを作成する


```domain/dto/BookDto.kt
package com.devtiro.bookstore.domain.dto

data class BookDto(
    var isbn: String,
    var title: String,
    var description: String,
    var image: String,
    var author: AuthorDto
)
```
③kotlin/com/devtiro/bookstore/Extensions.ktを作成する

```testkotlin/com/devtiro/bokkstore/Extensions.kt
package com.devtiro.bookstore

import com.devtiro.bookstore.domain.dto.AuthorDto
import com.devtiro.bookstore.domain.entities.AuthorEntity

fun AuthorEntity.toAuthorDto() = AuthorDto(
    id=this.id,
    name=this.name,
    age=this.age,
    description=this.description,
    image=this.image
    )

fun AuthorDto.toAuthorEntity() = AuthorEntity(
    id=this.id,
    name=this.name,
    age=this.age,
    description=this.description,
    image=this.image
    )
```
## Author Create

①services/AuthorService.ktを作成する

```services/AuthorService.kt
package com.devtiro.bookstore.services

import com.devtiro.bookstore.domain.entities.AuthorEntity

interface AuthorService{

    fun save(AuthorEntity: AuthorEntity): AuthorEntity
}
```

②services/impl/AuthorServiceImpl.ktを作成する

```services/impl/AuthorServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.services.AuthorService
import com.devtiro.bookstore.repositories.authorRepository
import com.devtiro.bookstore.services.AuthorService
import org.springframework.stereotype.Service


@Service
class AuthorServiceImpl (val authorRepository: AuthorService){

   override fun save(AuthorEntity: AuthorEntity): AuthorEntity{
    return authorRepository.save(AuthorEntity)
   }
}
```
## Testing Author Create

①test/resouces/application.propertiesを作成する

```test/resouces/application.properties
spring.application.name=bookstore

spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://localhost:5432/postgresql
spring.datasource.username=postgresql
spring.datasource.password=root
```
②test/kotlin/controllers/AuthorControllerTest.kt作成する

```test/kotlin/controllers/AuthorControllerTest.kt
package com.devtiro.bookstore.controllers

import org.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.AutoConfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.post

@SpringBootTest
@AutoConfigureMockMvc
class AuthorControllerTest @Autowired constructor(
    private val mockMvc: MockMvc,
    @MockBean val authorService: AuthorService
    ) {

    val objectMapper = ObjectMapper()

    @BeforeEach
    fun beforeEach()
    every {
        authorService.save(any())
    } answers {
        firstArg()
    }

    @Test
    fun `test that create Author saves the Author`(){
        every {
            authorService.save(any())
        } answers {
            firstArg()
        }


        mockMvc.post("/v1/authors"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(
                AuthorDto(
                    id = null,
                    name = "John Doe",
                    age = 30,
                    description = "author-image.jpeg",
                    image = "some-description.jpeg"
                )
            )
    }
    val expected = AuthorEntity(
        id = null,
        name = "John Doe",
        age = 30,
        description = "author-image.jpeg",
        image = "some-description.jpeg"
    )
}
    @Test
    fun `test that create Author returns a HTTP 201 status on a successful create`(){
        mockMvc.post("/v1/authors"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(
                AuthorDto(
                    id = null,
                    name = "John Doe",
                    age = 30,
                    description = "author-image.jpeg",
                    image = "some-description.jpeg"
                )
            )
        }.andExpect {
            status { isCreated() }
        }
    }
}
    
```
③controllers/AuthorsController.ktを編集する

```controllers/AuthorsController.kt
package com.devtiro.bookstore.controllers


import com.devtiro.bookstore.domain.dto.AuthorDto
import com.devtiro.bookstore.services.AuthorService
import com.devtiro.bookstore.services.toAuthorDto
import com.devtiro.bookstore.services.toAuthorEntity
import org.springframework.web.bind.annoatation.PostMapping
import org.springframework.web.bind.annoatation.RequestBody
import org.springframework.web.bind.annoatation.RestController

@RestController
class AuthorController(private val AuthorService: AuthorService) {

    @PostMapping(path = ["/v1/authors"])
    fun createAuthor(@RequestBody authorDto: AuthorDto): ResponseEntity<AuthorDto>{
        val createAuthor = authorService.save(authorDto.toAtuthorEntity()
        ).toAtuthorDto()
        return ResponseEntity(createAuthor, HttpStatus.CREATED)
    }
}
```
④Test/kotlin/com/devtiro/bookstore/TestDataUtil.ktを作成する

```Test/kotlin/com/devtiro/bookstore/TestDataUtil.kt
package com.devtiro.bookstore

fun testAuthorDtoA(id: Long? = null) = AuthorDto(
    id = id,
    name = "John Doe",
    age = 30,
    description = "author-image.jpeg",
    image = "some-description.jpeg"
    )
    
fun testAuthorEntityA(id: Long? = null) = AuthorEntity(
    id = id,
    name = "John Doe",
    age = 30,
    description = "author-image.jpeg",
    image = "some-description.jpeg"
    )
```
⑤test/kotlin/controllers/AuthorControllerTest.kt作成する

```test/kotlin/controllers/AuthorControllerTest.kt
package com.devtiro.bookstore.controllers

import org.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.AutoConfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.post

@SpringBootTest
@AutoConfigureMockMvc
class AuthorControllerTest @Autowired constructor(
    private val mockMvc: MockMvc,
    @MockBean val authorService: AuthorService
    ) {

    val objectMapper = ObjectMapper()

    @BeforeEach
    fun beforeEach()
    every {
        authorService.save(any())
    } answers {
        firstArg()
    }

    @Test
    fun `test that create Author saves the Author`(){
        every {
            authorService.save(any())
        } answers {
            firstArg()
        }


        mockMvc.post("/v1/authors"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(
                testAuthorDtoAuthorDto()
            )
    }
    val expected = AuthorEntity(
        id = null,
        name = "John Doe",
        age = 30,
        description = "author-image.jpeg",
        image = "some-description.jpeg"
    )
}
    @Test
    fun `test that create Author returns a HTTP 201 status on a successful create`(){
        mockMvc.post("/v1/authors"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(
                testAuthorDtoAuthorDto()
                )
        }.andExpect {
            status { isCreated() }
        }
    }
}
```
⑥test/kotlin/services/impl/AuthorServiceImplTest.kt作成する

```test/kotlin/services/impl/AuthorServiceImplTest.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.testAuthorEntityA
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annoatation.Autowired
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
class AuthorServiceImplTest @Autowired constructor(
    private underTest: AuthorServiceImpl,
    private val authorRepository: authorRepository){

    @Test
    fun `test that save persists the Author in the database`(){
       val saveAuthor = underTest.save(testAuthorEntityA())
       assertThat(saveAuthor.id).isNotNull()

       val recalledAuthor = authorRepository.findById(saveAuthor.id)
       assertThat(recalledAuthor).isNotNull()
       assertThat(saveAuthor.id).isEqualTo(
        testAuthorEntityA(id=saveAuthor.id)
       )
    }
}
```

## Author Read Many

①controllers/AuthorsController.ktを編集する

```controllers/AuthorsController.kt
package com.devtiro.bookstore.controllers


import com.devtiro.bookstore.domain.dto.AuthorDto
import com.devtiro.bookstore.services.AuthorService
import com.devtiro.bookstore.services.toAuthorDto
import com.devtiro.bookstore.services.toAuthorEntity
import org.springframework.web.bind.annoatation.*

@RestController
@RequestMapping(path = ["/v1/authors"])
class AuthorController(private val authorService: AuthorService) {

    @PostMapping
    fun createAuthor(@RequestBody authorDto: AuthorDto): ResponseEntity<AuthorDto>{
        val createAuthor = authorService.save(authorDto.toAtuthorEntity()
        ).toAtuthorDto()
        return ResponseEntity(createAuthor, HttpStatus.CREATED)
    }
    @GetMapping
    fun readManyAuthor(): List<AuthorDto>{
        
        return authorService.list().map{it.toAtuthorDto()}
    }
}
```
②services/impl/AuthorServiceImpl.ktを編集する

```services/impl/AuthorServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.services.AuthorService
import com.devtiro.bookstore.repositories.authorRepository
import com.devtiro.bookstore.services.AuthorService
import org.springframework.stereotype.Service

@Service
class AuthorServiceImpl (val authorRepository: AuthorService){

   override fun save(AuthorEntity: AuthorEntity): AuthorEntity{
    return authorRepository.save(AuthorEntity)
   }
}
```

③services/AuthorService.ktを作成する

```services/AuthorService.kt
package com.devtiro.bookstore.services

import com.devtiro.bookstore.domain.entities.AuthorEntity

interface AuthorService{

    fun save(AuthorEntity: AuthorEntity): AuthorEntity

    fun list(): List<AuthorEntity> {
        return authorRepository.findAll()
    }
}
```

## Test Author Read Many

①test/kotlin/controllers/AuthorControllerTest.ktを編集する

```test/kotlin/controllers/AuthorControllerTest.kt
package com.devtiro.bookstore.controllers

import com.ninjasquad.springmockk.MockBean
import io.mockk.empty
import io.mockk.verify
import org.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.AutoConfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.post

private const val AUTHORS_BASE_URL = "/v1/authors"

@SpringBootTest
@AutoConfigureMockMvc
class AuthorControllerTest @Autowired constructor(
    private val mockMvc: MockMvc,
    @MockBean val authorService: AuthorService
    ) 

    val objectMapper = ObjectMapper()

    @BeforeEach
    fun beforeEach()
    every {
        authorService.save(any())
    } answers {
        firstArg()
    }

    @Test
    fun `test that create Author saves the Author`(){
        every {
            authorService.save(any())
        } answers {
            firstArg()
        }

        mockMvc.post("AUTHORS_BASE_URL"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(
                testAuthorDtoAuthorDto()
            )
    }
    val expected = AuthorEntity(
        id = null,
        name = "John Doe",
        age = 30,
        image = "author-image.joeg",
        description = "author-image.jpeg"
    )
    verify{authorService.save(expected)}
}
    
@Test
    fun `test that list returns an empty list and HTTP 200 when no author in the database`(){
        every {
            authorService.list()
        } answers {
            enptyList()
        }

        mockMvc.get("AUTHORS_BASE_URL"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk() }
            content { json ( "[]")}
        }
    }

    @Test
    fun `test that list returns author and HTTP 200 when author in the database`(){
        every {
            authorService.list()
        } answers {
            ListOf(testAuthorEntityA(1))
        }

        mockMvc.get("AUTHORS_BASE_URL"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk() }
            content { jsonPath ( "$[0].id", equalTo(1))}
            content { jsonPath ( "$[0].name", equalTo("John Doe"))}
            content { jsonPath ( "$[0].age", equalTo(30))}
            content { jsonPath ( "$[0].description", equalTo("Some description"))}
            content { jsonPath ( "$[0].image", equalTo("author-image.jpeg"))}
        }
}

```


②test/kotlin/services/impl/AuthorServiceImpl.ktを編集する

```test/kotlin/services/impl/AuthorServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.testAuthorEntityA
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annoatation.Autowired
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
class AuthorServiceImplTest @Autowired constructor(
    private underTest: AuthorServiceImpl,
    private val authorRepository: authorRepository){

    @Test
    fun `test that save persists the Author in the database`(){
       val saveAuthor = underTest.save(testAuthorEntityA())
       assertThat(saveAuthor.id).isNotNull()

       val recalledAuthor = authorRepository.findById(saveAuthor.id)
       assertThat(recalledAuthor).isNotNull()
       assertThat(saveAuthor.id).isEqualTo(
        testAuthorEntityA(id=saveAuthor.id)
       )
    }

    @Test
    fun `test that list returns empty list when no author in the database`(){
       val result = underTest.list(testAuthorEntityA())
       assertThat(result).isEmpty()
    }

    @Test
    fun `test that list returns authors when present in the database`(){
       val saveAuthor = authorRepository.save(testAuthorEntityA())
       val expected = ListOf(saveAuthor)
       val result = underTest.list()
       assertThat(result).isEqualTo(expected)
    }
}
```

## Author Read One

①controllers/AuthorsController.ktを編集する

```controllers/AuthorsController.kt
package com.devtiro.bookstore.controllers


import com.devtiro.bookstore.domain.dto.AuthorDto
import com.devtiro.bookstore.services.AuthorService
import com.devtiro.bookstore.services.toAuthorDto
import com.devtiro.bookstore.services.toAuthorEntity
import org.springframework.web.bind.annoatation.*

@RestController
@RequestMapping(path = ["/v1/authors"])
class AuthorController(private val authorService: AuthorService) {

    @PostMapping
    fun createAuthor(@RequestBody authorDto: AuthorDto): ResponseEntity<AuthorDto> {
        val createAuthor = authorService.save(authorDto.toAtuthorEntity()
        ).toAtuthorDto()
        return ResponseEntity(createAuthor, HttpStatus.CREATED)
    }

    @GetMapping
    fun readManyAuthor(): List<AuthorDto> {
        return authorService.list().map{it.toAtuthorDto()}
    }

    @GetMapping(path = ["/{id}"])
    fun readOneAuthor(@PathVariable("id") id: Long): ResponseEntity<AuthorDto> {
        val readOneAuthor = authorService.get(id)?.toAtuthorDto()
        return foundAuthor?.let {
            ResponseEntity(it, HttpStatus.OK)
        } ?: ResponseEntity(HttpStatus.NOT_FOUND)
    }
}
```

②services/AuthorService.ktを編集する

```services/AuthorService.kt
package com.devtiro.bookstore.services

import com.devtiro.bookstore.domain.entities.AuthorEntity

interface AuthorService {

    fun save(AuthorEntity: AuthorEntity): AuthorEntity

    fun list(): List<AuthorEntity>

    fun list(id: Long): AuthorEntity?

}
```
③services/impl/AuthorServiceImpl.ktを編集する

```services/impl/AuthorServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.services.AuthorService
import com.devtiro.bookstore.repositories.authorRepository
import com.devtiro.bookstore.services.AuthorService
import org.springframework.stereotype.Service

@Service
class AuthorServiceImpl (private val authorRepository: authorRepository): AuthorService {

   override fun save(AuthorEntity: AuthorEntity): AuthorEntity {
      return authorRepository.save(AuthorEntity)
   }

   override fun list(): List<AuthorEntity> {
      return authorRepository.findAll()
   }

   override fun get(id: Long): AuthorEntity? {
      return authorRepository.findByIdOrNull(id)
   }
}
```

## Test Author Read One

①test/kotlin/controllers/AuthorControllerTest.ktを編集する

```test/kotlin/controllers/AuthorControllerTest.kt
package com.devtiro.bookstore.controllers

import com.ninjasquad.springmockk.MockBean
import io.mockk.empty
import io.mockk.verify
import org.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.AutoConfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.post

private const val AUTHORS_BASE_URL = "/v1/authors"

@SpringBootTest
@AutoConfigureMockMvc
class AuthorControllerTest @Autowired constructor(
    private val mockMvc: MockMvc,
    @MockBean val authorService: AuthorService
    ) 

    val objectMapper = ObjectMapper()

    @BeforeEach
    fun beforeEach()
    every {
        authorService.save(any())
    } answers {
        firstArg()
    }

    @Test
    fun `test that create Author saves the Author`(){
        every {
            authorService.save(any())
        } answers {
            firstArg()
        }

        mockMvc.post("AUTHORS_BASE_URL"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(
                testAuthorDtoAuthorDto()
            )
    }
    val expected = AuthorEntity(
        id = null,
        name = "John Doe",
        age = 30,
        image = "author-image.joeg",
        description = "author-image.jpeg"
    )
    verify{authorService.save(expected)}
}
    
@Test
    fun `test that list returns an empty list and HTTP 200 when no author in the database`(){
        every {
            authorService.list()
        } answers {
            enptyList()
        }

        mockMvc.get("AUTHORS_BASE_URL"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk() }
            content { json ( "[]")}
        }


    @Test
    fun `test that list returns author and HTTP 200 when author in the database`(){
        every {
            authorService.list()
        } answers {
            ListOf(testAuthorEntityA(1))
        }

        mockMvc.get("AUTHORS_BASE_URL"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk() }
            content { jsonPath ( "$[0].id", equalTo(1))}
            content { jsonPath ( "$[0].name", equalTo("John Doe"))}
            content { jsonPath ( "$[0].age", equalTo(30))}
            content { jsonPath ( "$[0].description", equalTo("Some description"))}
            content { jsonPath ( "$[0].image", equalTo("author-image.jpeg"))}
        }

@Test
    fun `test that get returns HTTP 404 when author in the database`(){
        every {
            authorService.get(any())
        } answers {
            null
        }

        mockMvc.get("${AUTHORS_BASE_URL}/999"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }}.andExpect {
            status { isNotFound() }
}

@Test
    fun `test that get returns HTTP 200 and author when author found`(){
        every {
            authorService.get(any())
        } answers {
            testAuthorEntityA(id=999)
        }

        mockMvc.get("${AUTHORS_BASE_URL}/999"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }}.andExpect {
            status { isOk() }
            content { jsonPath ( "$.id", equalTo(999))}
            content { jsonPath ( "$.name", equalTo("John Doe"))}
            content { jsonPath ( "$.age", equalTo(30))}
            content { jsonPath ( "$.description", equalTo("Some description"))}
            content { jsonPath ( "$.image", equalTo("author-image.jpeg"))}
        }
    }
}
```
②test/kotlin/services/impl/AuthorServiceImpl.ktを編集する

```test/kotlin/services/impl/AuthorServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.testAuthorEntityA
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annoatation.Autowired
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
class AuthorServiceImplTest @Autowired constructor(
   private underTest: AuthorServiceImpl,
   private val authorRepository: authorRepository){

   @Test
   fun `test that save persists the Author in the database`(){
      val saveAuthor = underTest.save(testAuthorEntityA())
      assertThat(saveAuthor.id).isNotNull()

      val recalledAuthor = authorRepository.findById(saveAuthor.id)
      assertThat(recalledAuthor).isNotNull()
      assertThat(saveAuthor.id).isEqualTo(
      testAuthorEntityA(id=saveAuthor.id)
      )
   }

   @Test
   fun `test that list returns empty list when no author in the database`(){
      val result = underTest.list(testAuthorEntityA())
      assertThat(result).isEmpty()
   }

   @Test
   fun `test that list returns authors when present in the database`(){
      val saveAuthor = authorRepository.save(testAuthorEntityA())
      val expected = ListOf(saveAuthor)
      val result = underTest.list()
      assertThat(result).isEqualTo(expected)
   }

   @Test
   fun `test that get returns null when author not present in the database`(){
      val result = underTest.get(999)
      assertThat(result).isNull()
   }

   @Test
   fun `test that get returns author when author is present in the database`(){
      val saveAuthor = authorRepository.save(testAuthorEntityA())
      val result = underTest.get(saveAuthor.id)
      assertThat(result).isEqualTo(saveAuthor)
   }

}
```
## Author Full Update

①services/impl/AuthorServiceImpl.ktを編集する

```services/impl/AuthorServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.services.AuthorService
import com.devtiro.bookstore.repositories.authorRepository
import com.devtiro.bookstore.services.AuthorService
import org.springframework.stereotype.Service

@Service
class AuthorServiceImpl (private val authorRepository: authorRepository): AuthorService {

   override fun save(AuthorEntity: AuthorEntity): AuthorEntity {
      require(null == AuthorEntity.id)
      return authorRepository.save(AuthorEntity)
   }

   override fun list(): List<AuthorEntity> {
      return authorRepository.findAll()
   }

   override fun get(id: Long): AuthorEntity? {
      return authorRepository.findByIdOrNull(id)
   }

   @Transactional
   override fun get(id: Long, authorEntity: AuthorEntity): AuthorEntity {
      check(authorRepository.existsById(id))
      val normalisedAuthor = authorEntity.copy(id=id)
      return authorRepository.save(normalisedAuthor)
   }
}
```
②services/AuthorService.ktを編集する

```services/AuthorService.kt
package com.devtiro.bookstore.services

import com.devtiro.bookstore.domain.entities.AuthorEntity

interface AuthorService {

    fun save(AuthorEntity: AuthorEntity): AuthorEntity

    fun list(): List<AuthorEntity>

    fun list(id: Long): AuthorEntity?

    fun fullUpdate(id: Long, authorEntity: AuthorEntity): AuthorEntity

}
```
③controllers/AuthorsController.ktを編集する

```controllers/AuthorsController.kt
package com.devtiro.bookstore.controllers


import com.devtiro.bookstore.domain.dto.AuthorDto
import com.devtiro.bookstore.services.AuthorService
import com.devtiro.bookstore.services.toAuthorDto
import com.devtiro.bookstore.services.toAuthorEntity
import org.springframework.web.bind.annoatation.*

@RestController
@RequestMapping(path = ["/v1/authors"])
class AuthorController(private val authorService: AuthorService) {

    @PostMapping
    fun createAuthor(@RequestBody authorDto: AuthorDto): ResponseEntity<AuthorDto> {
        return try {
            val createAuthor = authorService.create(
                authorDto.toAtuthorEntity()
            ).toAtuthorDto()
            ResponseEntity(createAuthor, HttpStatus.CREATE)

        } catch (ex: IllegalArgumentException) {
            ResponseEntity(HttpStatus.NOT_FOUND)
        }
    }

    @GetMapping
    fun readManyAuthor(): List<AuthorDto> {
        return authorService.list().map{it.toAtuthorDto()}
    }

    @GetMapping(path = ["/{id}"])
    fun readOneAuthor(@PathVariable("id") id: Long): ResponseEntity<AuthorDto> {
        val readOneAuthor = authorService.get(id)?.toAtuthorDto()
        return foundAuthor?.let {
            ResponseEntity(it, HttpStatus.OK)
        } ?: ResponseEntity(HttpStatus.NOT_FOUND)
    }

    @PutMapping(path = ["/{id}"])
    fun fullUpdateAuthor(@PathVariable("id") id:Long, @RequestBody authorDto: AuthorDto): ResponseEntity<AuthorDto> {
        return try {
            val updateAuthor = authorService.fullUpdate(id, authorDto.toAtuthorEntity())
            ResponseEntity(updateAuthor.toAtuthorDto(), HttpStatus.OK)

        } catch (ex: IllegalArgumentException) {
            ResponseEntity(HttpStatus.BAD_REQUEST)
        }
    }
}
```

## TEST Author Full Update

①test/kotlin/controllers/AuthorControllerTest.ktを編集する

```test/kotlin/controllers/AuthorControllerTest.kt
package com.devtiro.bookstore.controllers

import com.ninjasquad.springmockk.MockBean
import io.mockk.empty
import io.mockk.verify
import org.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.AutoConfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.post

private const val AUTHORS_BASE_URL = "/v1/authors"

@SpringBootTest
@AutoConfigureMockMvc
class AuthorControllerTest @Autowired constructor(
    private val mockMvc: MockMvc,
    @MockBean val authorService: AuthorService
    ) 

    val objectMapper = ObjectMapper()

    @BeforeEach
    fun beforeEach()
        every {
            authorService.save(any())
        } answers {
            firstArg()
    }

@Test
fun `test that create Author saves the Author`(){
    every {
        authorService.save(any())
    } answers {
        firstArg()
    }

    mockMvc.post("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(
            testAuthorDtoAuthorDto()
        )
    }
    val expected = AuthorEntity(
        id = null,
        name = "John Doe",
        age = 30,
        image = "author-image.jpeg",
        description = "some description"
    )
    verify{authorService.create(expected)}
}

@Test
fun `test that create Author returns a HTTP 201 status no a successful create`(){
    mockMvc.get("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(
            testAuthorDtoA()
        )
    }.andExpect {
        status { isCreated() }
    }
}

@Test
fun `test that create Author returns HTTP 400 when IllegalArgumentException is thrown`(){
every {
    authorService.create(any())
} throws (IllegalArgumentException())

    mockMvc.get("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(
            testAuthorDtoA()
        )
    }.andExpect {
        status { isBadRequest() }
    }
}


@Test
fun `test that list returns an empty list and HTTP 200 when no author in the database`(){
    every {
        authorService.list()
    } answers {
        enptyList()
    }

    mockMvc.get("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
    } andExpect {
        status { isOk() }
        content { json ( "[]")}
}


@Test
fun `test that list returns author and HTTP 200 when author in the database`(){
    every {
        authorService.list()
    } answers {
        ListOf(testAuthorEntityA(1))
    }

    mockMvc.get("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
    }.andExpect {
        status { isOk() }
        content { jsonPath ( "$[0].id", equalTo(1))}
        content { jsonPath ( "$[0].name", equalTo("John Doe"))}
        content { jsonPath ( "$[0].age", equalTo(30))}
        content { jsonPath ( "$[0].description", equalTo("Some description"))}
        content { jsonPath ( "$[0].image", equalTo("author-image.jpeg"))}
    }

@Test
fun `test that get returns HTTP 404 when author in the database`(){
    every {
        authorService.get(any())
    } answers {
        null
    }

    mockMvc.get("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
    }}.andExpect {
        status { isNotFound() }
}

@Test
fun `test that get returns HTTP 200 and author when author found`(){
    every {
        authorService.get(any())
    } answers {
        testAuthorEntityA(id=999)
    }

    mockMvc.get("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
    }}.andExpect {
        status { isOk() }
        content { jsonPath ( "$.id", equalTo(999))}
        content { jsonPath ( "$.name", equalTo("John Doe"))}
        content { jsonPath ( "$.age", equalTo(30))}
        content { jsonPath ( "$.description", equalTo("Some description"))}
        content { jsonPath ( "$.image", equalTo("author-image.jpeg"))}
        }
    }
}
```
②test/kotlin/services/impl/AuthorServiceImpl.ktを編集する

```test/kotlin/services/impl/AuthorServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.testAuthorEntityA
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annoatation.Autowired
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
class AuthorServiceImplTest @Autowired constructor(
   private underTest: AuthorServiceImpl,
   private val authorRepository: authorRepository){

   @Test
   fun `test that save persists the Author in the database`(){
      val saveAuthor = underTest.save(testAuthorEntityA())
      assertThat(saveAuthor.id).isNotNull()

      val recalledAuthor = authorRepository.findById(saveAuthor.id)
      assertThat(recalledAuthor).isNotNull()
      assertThat(saveAuthor.id).isEqualTo(
      testAuthorEntityA(id=saveAuthor.id)
      )
   }

   @Test
   fun `test that an Author with an ID throws an IllegalArgumentException`(){
      assertThrows<IllegalArgumentException>{
         val existingAuthor = testAuthorEntityA(id=999)
         underTest.create(existingAuthor)
      }
   }

   @Test
   fun `test that list returns empty list when no author in the database`(){
      val result = underTest.list(testAuthorEntityA())
      assertThat(result).isEmpty()
   }

   @Test
   fun `test that list returns authors when present in the database`(){
      val saveAuthor = authorRepository.save(testAuthorEntityA())
      val expected = ListOf(saveAuthor)
      val result = underTest.list()
      assertThat(result).isEqualTo(expected)
   }

   @Test
   fun `test that get returns null when author not present in the database`(){
      val result = underTest.get(999)
      assertThat(result).isNull()
   }

   @Test
   fun `test that get returns author when author is present in the database`(){
      val saveAuthor = authorRepository.save(testAuthorEntityA())
      val result = underTest.get(saveAuthor.id)
      assertThat(result).isEqualTo(saveAuthor)
   }

   @Test
   fun `test that full update successfully update the author in the database`(){
      val existingAuthor = authorRepository.save(testAuthorEntityA())
      val existingAuthorId = existingAuthor.id
      val updateAuthorId = AuthorEntityB(
         id = existingAuthorId,
         name = "Don Joe",
         age = 45,
         description = "Some other description",
         image = "some-other-image.jpeg",
      )
      val result = underTest.fullUpdate(existingAuthorId, updateAuthor)
      assertThat(result).isEqualTo(updateAuthor)

      val retrievedAuthor = authorRepository.findByIdOrNull(existingAuthorId)
      assertThat(retrievedAuthor).isNotNull()
      assertThat(retrievedAuthor).isEqualTo(updateAuthor)
   }

   @Test
   fun `test that full update Author throws IllegalStateException when Author does not exist in the database`(){
      assertThrows<IllegalStateException> {
      val nonExistingAuthorId = 999
      val updateAuthor = testAuthorEntityB(id=nonExistingAuthorId)
      underTest.fullUpdate(nonExistingAuthorIdm updateAuthor)
      }
   }
}
```

③test/kotlin/services/impl/TestDataUtil.ktを作成する

```test/kotlin/services/impl/TestDataUtil.kt
package com.devtiro.bookstore

import com.devtiro.bookstore.domain.dto.AuthorDto
import com.devtiro.bookstore.domain.entities.AuthorEntity

fun AuthorEntity.toAuthorDto() = AuthorDto(
    id=this.id,
    name=this.name,
    age=this.age,
    description=this.description,
    image=this.image
    )

fun AuthorDto.toAuthorEntity() = AuthorEntity(
    id=this.id,
    name=this.name,
    age=this.age,
    description=this.description,
    image=this.image
    )
```

## Author Partial Update

①controllers/AuthorsController.ktを編集する

```controllers/AuthorsController.kt
package com.devtiro.bookstore.controllers


import com.devtiro.bookstore.domain.dto.AuthorDto
import com.devtiro.bookstore.services.AuthorService
import com.devtiro.bookstore.services.toAuthorDto
import com.devtiro.bookstore.services.toAuthorEntity
import org.springframework.web.bind.annoatation.*

@RestController
@RequestMapping(path = ["/v1/authors"])
class AuthorController(private val authorService: AuthorService) {

    @PostMapping
    fun createAuthor(@RequestBody authorDto: AuthorDto): ResponseEntity<AuthorDto> {
        return try {
            val createAuthor = authorService.create(
                authorDto.toAtuthorEntity()
            ).toAtuthorDto()
            ResponseEntity(createAuthor, HttpStatus.CREATE)

        } catch (ex: IllegalArgumentException) {
            ResponseEntity(HttpStatus.NOT_FOUND)
        }
    }

    @GetMapping
    fun readManyAuthor(): List<AuthorDto> {
        return authorService.list().map{it.toAtuthorDto()}
    }

    @GetMapping(path = ["/{id}"])
    fun readOneAuthor(@PathVariable("id") id: Long): ResponseEntity<AuthorDto> {
        val readOneAuthor = authorService.get(id)?.toAtuthorDto()
        return foundAuthor?.let {
            ResponseEntity(it, HttpStatus.OK)
        } ?: ResponseEntity(HttpStatus.NOT_FOUND)
    }

    @PutMapping(path = ["/{id}"])
    fun fullUpdateAuthor(@PathVariable("id") id:Long, @RequestBody authorDto: AuthorDto): ResponseEntity<AuthorDto> {
        return try {
            val updateAuthor = authorService.fullUpdate(id, authorDto.toAtuthorEntity())
            ResponseEntity(updateAuthor.toAtuthorDto(), HttpStatus.OK)

        } catch (ex: IllegalArgumentException) {
            ResponseEntity(HttpStatus.BAD_REQUEST)
        }
    }

    @PatchMapping(path = ["/{id}"])
    fun partiaUpdateAuthor(@PathVariable("id") id:Long, @RequestBody authorDto: AuthorUpdateRequest): ResponseEntity<AuthorDto> {
        return try {
            val updateAuthor = authorService.fullUpdate(id, authorDto.toAtuthorEntity())
            ResponseEntity(updateAuthor.toAtuthorDto(), HttpStatus.OK)

        } catch (ex: IllegalArgumentException) {
            ResponseEntity(HttpStatus.BAD_REQUEST)
        }
    }

    @PatchMapping(path = ["/{id}"])
    fun partiaUpdateAuthor(@PathVariable("id") id:Long, @RequestBody authorUpdateRequest: AuthorUpdateRequest): ResponseEntity<AuthorDto> {
        return try {
            val updatedAuthor = authorService.fullUpdate(id, authorDto.toAtuthorEntity())
            ResponseEntity(updatedAuthor.toAtuthorDto(), HttpStatus.OK)

        } catch (ex: IllegalStateException) {
            ResponseEntity(HttpStatus.BAD_REQUEST)
        }
    }
}
```
②domain/dto/AuthorUpdateRequestDto.ktを作成する

```domain/dto/AuthorUpdateRequestDto.kt
package com.devtiro.bookstore.domain.dto

data class AuthorUpdateRequestDTO(
    val id: Long?,
    val name: String?,
    val age: Int?,
    val description: String?,
    val image: String
    )
```

③services/AuthorService.ktを編集する

```services/AuthorService.kt
package com.devtiro.bookstore.services

import com.devtiro.bookstore.domain.entities.AuthorEntity

interface AuthorService {

    fun save(AuthorEntity: AuthorEntity): AuthorEntity

    fun list(): List<AuthorEntity>

    fun list(id: Long): AuthorEntity?

    fun fullUpdate(id: Long, authorEntity: AuthorEntity): AuthorEntity

    fun fullUpdate(id: Long, AuthorUpdate: AuthorUpdateRequest): AuthorEntity
}

```

④domain/AuthorUpdateRequest.ktを作成する

```domain/AuthorUpdateRequest.kt
package com.devtiro.bookstore.domain

data class AuthorUpdateRequest(
    val id: Long?,
    val name: String?,
    val age: Int?,
    val description: String?,
    val image: String
    )

```
⑤kotlin/com/devtiro/bookstore/Extensions.ktを編集する

```kotlin/com/devtiro/bookstore/Extensions.kt
package com.devtiro.bookstore

import com.devtiro.bookstore.domain.dto.AuthorDto
import com.devtiro.bookstore.domain.entities.AuthorEntity

fun AuthorEntity.toAuthorDto() = AuthorDto(
    id=this.id,
    name=this.name,
    age=this.age,
    description=this.description,
    image=this.image
    )

fun AuthorDto.toAuthorEntity() = AuthorEntity(
    id=this.id,
    name=this.name,
    age=this.age,
    description=this.description,
    image=this.image
    )

fun AuthorUpdateRequestDto.toAuthorUpdateRequestDto() = AuthorUpdateRequest(
    id=this.id,
    name=this.name,
    age=this.age,
    description=this.description,
    image=this.image
    )
```

## Test Uthor Partial Update

①test/kotlin/controllers/AuthorControllerTest.ktを編集する

```test/kotlin/controllers/AuthorControllerTest.kt
package com.devtiro.bookstore.controllers

import com.ninjasquad.springmockk.MockBean
import io.mockk.empty
import io.mockk.verify
import org.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.AutoConfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.post

private const val AUTHORS_BASE_URL = "/v1/authors"

@SpringBootTest
@AutoConfigureMockMvc
class AuthorControllerTest @Autowired constructor(
    private val mockMvc: MockMvc,
    @MockBean val authorService: AuthorService
    ) 

    val objectMapper = ObjectMapper()

    @BeforeEach
    fun beforeEach()
        every {
            authorService.save(any())
        } answers {
            firstArg()
    }

@Test
fun `test that create Author saves the Author`(){
    every {
        authorService.save(any())
    } answers {
        firstArg()
    }

    mockMvc.post("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(
            testAuthorDtoAuthorDto()
        )
    }
    val expected = AuthorEntity(
        id = null,
        name = "John Doe",
        age = 30,
        image = "author-image.jpeg",
        description = "some description"
    )
    verify{authorService.create(expected)}
}

@Test
fun `test that create Author returns a HTTP 201 status no a successful create`(){
    mockMvc.get("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(
            testAuthorDtoA()
        )
    }.andExpect {
        status { isCreated() }
    }
}

@Test
fun `test that create Author returns HTTP 400 when IllegalArgumentException is thrown`(){
every {
    authorService.create(any())
} throws (IllegalArgumentException())

    mockMvc.get("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(
            testAuthorDtoA()
        )
    }.andExpect {
        status { isBadRequest() }
    }
}

@Test
fun `test that list returns an empty list and HTTP 200 when no author in the database`(){
    every {
        authorService.list()
    } answers {
        enptyList()
}

    mockMvc.get("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
    } andExpect {
        status { isOk() }
        content { json ( "[]")}
}

@Test
fun `test that list returns author and HTTP 200 when author in the database`(){
    every {
        authorService.list()
    } answers {
        ListOf(testAuthorEntityA(1))
    }

    mockMvc.get("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
    }.andExpect {
        status { isOk() }
        content { jsonPath ( "$[0].id", equalTo(1))}
        content { jsonPath ( "$[0].name", equalTo("John Doe"))}
        content { jsonPath ( "$[0].age", equalTo(30))}
        content { jsonPath ( "$[0].description", equalTo("Some description"))}
        content { jsonPath ( "$[0].image", equalTo("author-image.jpeg"))}
    }

@Test
fun `test that get returns HTTP 404 when author in the database`(){
    every {
        authorService.get(any())
    } answers {
        null
    }

    mockMvc.get("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
    }}.andExpect {
        status { isNotFound() }
}

@Test
fun `test that get returns HTTP 200 and author when author found`(){
    every {
        authorService.get(any())
    } answers {
        testAuthorEntityA(id=999)
    }

    mockMvc.get("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
    }}.andExpect {
        status { isOk() }
        content { jsonPath ( "$.id", equalTo(999))}
        content { jsonPath ( "$.name", equalTo("John Doe"))}
        content { jsonPath ( "$.age", equalTo(30))}
        content { jsonPath ( "$.description", equalTo("Some description"))}
        content { jsonPath ( "$.image", equalTo("author-image.jpeg"))}
        }
    }

@Test
fun `test that full update Author return HTTP 200 and updated Author on successful call`(){
    every {
        authorService.fullUpdate(any(),any())
    } answers {
        secondArg()
    }
    
    mockMvc.put("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(testAuthorDtoA(id=999))
    }}.andExpect {
        status { isOk() }
        content { jsonPath ( "$.id", equalTo(999))}
        content { jsonPath ( "$.name", equalTo("John Doe"))}
        content { jsonPath ( "$.age", equalTo(30))}
        content { jsonPath ( "$.description", equalTo("Some description"))}
        content { jsonPath ( "$.image", equalTo("author-image.jpeg"))}
    }

@Test
fun `test that full update Author return HTTP 400 on IllegalStateException`(){
    every {
        authorService.fullUpdate(any(),any())
    } throws(IllegalStateException)
    
    mockMvc.put("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(testAuthorDtoA(id=999))
    }}.andExpect {
        status { isBadRequest() }
    }

@Test
fun `test that partial update Author return HTTP 400 on IllegalStateException`(){
    every {
        authorService.partialUpdate(any(),any())
    } throws(IllegalStateException)
    
    mockMvc.pacth("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(
            testAuthorUpdateRequestDtoA(id=999L)
        )
    } andExpect {
        status { isBadRequest()}
    }

@Test
fun `test that partial update Author return HTTP 200 and update Author`(){
    every {
        authorService.partialUpdate(any(),any())
    } answers {
        testAuthorEntityA(id=999)
    }

    mockMvc.pacth("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(
            testAuthorUpdateRequestDtoA(id=999L)
        )
    } andExpect {
        status { isOk()}
        content { jsonPath ( "$.id", equalTo(999))}
        content { jsonPath ( "$.name", equalTo("John Doe"))}
        content { jsonPath ( "$.age", equalTo(30))}
        content { jsonPath ( "$.description", equalTo("Some description"))}
        content { jsonPath ( "$.image", equalTo("author-image.jpeg"))}   
    }
}
```

②test/kotlin/services/impl/AuthorServiceImpl.ktを編集する

```test/kotlin/services/impl/AuthorServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.testAuthorEntityA
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annoatation.Autowired
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
class AuthorServiceImplTest @Autowired constructor(
   private underTest: AuthorServiceImpl,
   private val authorRepository: authorRepository){

   @Test
   fun `test that save persists the Author in the database`(){
      val saveAuthor = underTest.save(testAuthorEntityA())
      assertThat(saveAuthor.id).isNotNull()

      val recalledAuthor = authorRepository.findById(saveAuthor.id)
      assertThat(recalledAuthor).isNotNull()
      assertThat(saveAuthor.id).isEqualTo(
      testAuthorEntityA(id=saveAuthor.id)
      )
   }

   @Test
   fun `test that an Author with an ID throws an IllegalArgumentException`(){
      assertThrows<IllegalArgumentException>{
         val existingAuthor = testAuthorEntityA(id=999)
         underTest.create(existingAuthor)
      }
   }

   @Test
   fun `test that list returns empty list when no author in the database`(){
      val result = underTest.list(testAuthorEntityA())
      assertThat(result).isEmpty()
   }

   @Test
   fun `test that list returns authors when present in the database`(){
      val saveAuthor = authorRepository.save(testAuthorEntityA())
      val expected = ListOf(saveAuthor)
      val result = underTest.list()
      assertThat(result).isEqualTo(expected)
   }

   @Test
   fun `test that get returns null when author not present in the database`(){
      val result = underTest.get(999)
      assertThat(result).isNull()
   }

   @Test
   fun `test that get returns author when author is present in the database`(){
      val saveAuthor = authorRepository.save(testAuthorEntityA())
      val result = underTest.get(saveAuthor.id)
      assertThat(result).isEqualTo(saveAuthor)
   }

   @Test
   fun `test that full update successfully update the author in the database`(){
      val existingAuthor = authorRepository.save(testAuthorEntityA())
      val existingAuthorId = existingAuthor.id
      val updateAuthorId = AuthorEntityB(
         id = existingAuthorId,
         name = "Don Joe",
         age = 45,
         description = "Some other description",
         image = "some-other-image.jpeg",
      )
      val result = underTest.fullUpdate(existingAuthorId, updateAuthor)
      assertThat(result).isEqualTo(updateAuthor)

      val retrievedAuthor = authorRepository.findByIdOrNull(existingAuthorId)
      assertThat(retrievedAuthor).isNotNull()
      assertThat(retrievedAuthor).isEqualTo(updateAuthor)
   }

   @Test
   fun `test that full update Author throws IllegalStateException when Author does not exist in the database`(){
      assertThrows<IllegalStateException> {
      val nonExistingAuthorId = 999L
      val updateAuthor = testAuthorEntityB(id=nonExistingAuthorId)
      underTest.fullUpdate(nonExistingAuthorIdm updateAuthor)
      }
   }

   @Test
   fun `test that partial update Author throws IllegalStateException when Author does not exist in the database`(){
      assertThrows<IllegalStateException> {
      val nonExistingAuthorId = 999L
      val updateRequest = testAuthorUpdateRequestA(id=nonExistingAuthorId)
      underTest.partialUpdate(nonExistingAuthorId, updateAuthor)
  }
}

   @Test
   fun `test that partial update Author throws does not update Author when all values exist are null`(){
      val existingAuthor = authorRepository.save(testAuthorEntityA())
      val updateAuthor = underTest.partialUpdate(existingAuthor.id!!, AuthorUpdateRequest())
      assertThat(updateAuthor).isEqualTo(existingAuthor)
  }
   @Test
   fun `test that partial update Author updates author name`(){
      val newName = "New Name"
      val AuthorUpdateRequest = authorUpdateRequest(
         name = newName
      )
      
      val AuthorUpdateRequest = authorUpdateRequest(
         name = newName
      )
      assertThatAuthorPartialUpdateIsUpdaed(
      existingAuthor = existingAuthor,
      expectedAuthor = expectedAuthor,
      authorUpdateRequest = authorUpdateRequest
      )
   }

   private fun assertThatAuthorPartialUpdateIsUpdaed(
      existingAuthor: AuthorEntity,
      expectedAuthor: AuthorEntity,
      authorUpdate: AuthorUpdateRequest
   ) {
      val savedExistingAuthor = authorRepository.save(existingAuthor)
      val existingAuthorId = existingAuthor.id!!
      val updateAuthor = underTest.partialUpdate(
         existingAuthorsId, AuthorUpdateRequest(
      ))
      val expected = expectedAuthor.copy(id=existingAuthorId)
      assertThat(updatedAuthor).isEqualTo(expected)

      val retrievedAuthor = authorRepository.findByIdOrNull(existingAuthorId)
      assertThat(retrievedAuthor).isNotNull()

      assertThat(updatedAuthor).isEqualTo(expected)
   }
}

@Test
fun `test that partial update Author updates author age`(){
   val newAge = "New Age"
   val existingAuthor = existingAuthorEntityA()
   val expectedAuthor = existingAuthor.copy(
         age = newAge
      )
      val AuthorUpdateRequest = authorUpdateRequest(
         age = newAge
      )
      assertThatAuthorPartialUpdateIsUpdaed(
         existingAuthor = existingAuthor,
         expectedAuthor = expectedAuthor,
         authorUpdateRequest = authorUpdateRequest,
      )
}

@Test
fun `test that partial update Author updates author description`(){
   val newDescription = "A new description"
   val existingAuthor = existingAuthorEntityA()
   val expectedAuthor = existingAuthor.copy(
         description = newDescription
      )
      val AuthorUpdateRequest = authorUpdateRequest(
         description = newDescription
      )
      assertThatAuthorPartialUpdateIsUpdaed(
         existingAuthor = existingAuthor,
         expectedAuthor = expectedAuthor,
         authorUpdateRequest = authorUpdateRequest,
      )
}

@Test
fun `test that partial update Author updates author image`(){
   val newImage = "A new image"
   val existingAuthor = existingAuthorEntityA()
   val expectedAuthor = existingAuthor.copy(
         image = newImage
      )
      val AuthorUpdateRequest = authorUpdateRequest(
         image = "newImage"
      )
      assertThatAuthorPartialUpdateIsUpdaed(
         existingAuthor = existingAuthor,
         expectedAuthor = expectedAuthor,
         authorUpdateRequest = authorUpdateRequest,
      )
}
```

③test/kotlin/services/impl/TestDataUtil.ktを編集する

```test/kotlin/services/impl/TestDataUtil.kt
package com.devtiro.bookstore

import com.devtiro.bookstore.domain.dto.AuthorDto
import com.devtiro.bookstore.domain.entities.AuthorEntity

fun testAuthorDtoA(id: Long? = null) = AuthorDto(
    id = id,
    name = "John Doe",
    age = 30,
    description = "some description"
    image = "author-image.jpeg",
)

fun testAuthorEntityA(id: Long? = null) = AuthorEntity(
    id = id,
    name = "John Doe",
    age = 30,
    description = "some description"
    image = "author-image.jpeg",
)

fun testAuthorEntityB(id: Long? = null) = AuthorEntity(
    id = null,
    name = "John Doe",
    age = 30,
    description = "some description"
    image = "author-image.jpeg",
)

fun testAuthorUpdateRequestDtoA(id: Long? = null) = AuthorUpdateRequestDto(
    id = null,
    name = "John Doe",
    age = 30,
    description = "some description"
    image = "author-image.jpeg",
)

fun testAuthorUpdateRequest(id: Long? = null) = AuthorUpdateRequest(
    id = null,
    name = "John Doe",
    age = 30,
    description = "some description"
    image = "author-image.jpeg",
)
```
④domain/AuthorUpdateRequest.ktを編集する

```domain/AuthorUpdateRequest.kt
package com.devtiro.bookstore.domain

data class AuthorUpdateRequest(
    val id: Long? = null,
    val name: String? = null,,
    val age: Int? = null,,
    val description: String? = null,,
    val image: String = null
    )
```

## Author Delete

①controllers/AuthorsController.ktを作成する

```controllers/AuthorsController.kt
package com.devtiro.bookstore.controllers


import com.devtiro.bookstore.domain.dto.AuthorDto
import com.devtiro.bookstore.services.AuthorService
import com.devtiro.bookstore.services.toAuthorDto
import com.devtiro.bookstore.services.toAuthorEntity
import org.springframework.web.bind.annoatation.*

@RestController
@RequestMapping(path = ["/v1/authors"])
class AuthorController(private val authorService: AuthorService) {

    @PostMapping
    fun createAuthor(@RequestBody authorDto: AuthorDto): ResponseEntity<AuthorDto> {
        return try {
            val createAuthor = authorService.create(
                authorDto.toAtuthorEntity()
            ).toAtuthorDto()
            ResponseEntity(createAuthor, HttpStatus.CREATE)

        } catch (ex: IllegalArgumentException) {
            ResponseEntity(HttpStatus.NOT_FOUND)
        }
    }

    @GetMapping
    fun readManyAuthor(): List<AuthorDto> {
        return authorService.list().map{it.toAtuthorDto()}
    }

    @GetMapping(path = ["/{id}"])
    fun readOneAuthor(@PathVariable("id") id: Long): ResponseEntity<AuthorDto> {
        val readOneAuthor = authorService.get(id)?.toAtuthorDto()
        return foundAuthor?.let {
            ResponseEntity(it, HttpStatus.OK)
        } ?: ResponseEntity(HttpStatus.NOT_FOUND)
    }

    @PutMapping(path = ["/{id}"])
    fun fullUpdateAuthor(@PathVariable("id") id:Long, @RequestBody authorDto: AuthorDto): ResponseEntity<AuthorDto> {
        return try {
            val updateAuthor = authorService.fullUpdate(id, authorDto.toAtuthorEntity())
            ResponseEntity(updateAuthor.toAtuthorDto(), HttpStatus.OK)

        } catch (ex: IllegalArgumentException) {
            ResponseEntity(HttpStatus.BAD_REQUEST)
        }
    }

    @PatchMapping(path = ["/{id}"])
    fun partiaUpdateAuthor(@PathVariable("id") id:Long, @RequestBody authorUpdateRequest: AuthorUpdateRequest): ResponseEntity<AuthorDto> {
        return try {
            val updatedAuthor = authorService.fullUpdate(id, authorDto.toAtuthorEntity())
            ResponseEntity(updatedAuthor.toAtuthorDto(), HttpStatus.OK)

        } catch (ex: IllegalStateException) {
            ResponseEntity(HttpStatus.BAD_REQUEST)
        }
    }

    @DeleteMapping(path = ["/{id}"])
    fun deleteAuthor(@PathVariable("id") id:Long): ResponseEntity<Unit> {
        authorService.delete(id)
        return ResponseEntity(HttpStatus.NOT_CONTENT)
    }
}
```
②services/AuthorService.ktを編集する

```services/AuthorService.kt
package com.devtiro.bookstore.services

import com.devtiro.bookstore.domain.entities.AuthorEntity

interface AuthorService {

    fun save(AuthorEntity: AuthorEntity): AuthorEntity

    fun list(): List<AuthorEntity>

    fun list(id: Long): AuthorEntity?

    fun fullUpdate(id: Long, authorEntity: AuthorEntity): AuthorEntity

    fun fullUpdate(id: Long, AuthorUpdate: AuthorUpdateRequest): AuthorEntity

    fun delete(id: Long)
}
```

③test/kotlin/services/impl/AuthorServiceImpl.ktを編集する

```test/kotlin/services/impl/AuthorServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.services.AuthorService
import com.devtiro.bookstore.repositories.authorRepository
import com.devtiro.bookstore.services.AuthorService
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional;

@Service
class AuthorServiceImpl (private val authorRepository: authorRepository): AuthorService {

   override fun save(AuthorEntity: AuthorEntity): AuthorEntity {
      require(null == AuthorEntity.id)
      return authorRepository.save(AuthorEntity)
   }

   override fun list(): List<AuthorEntity> {
      return authorRepository.findAll()
   }

   override fun get(id: Long): AuthorEntity? {
      return authorRepository.findByIdOrNull(id)
   }

   @Transactional
   override fun get(id: Long, authorEntity: AuthorEntity): AuthorEntity {
      check(authorRepository.existsById(id))
      val normalisedAuthor = authorEntity.copy(id=id)
      return authorRepository.save(normalisedAuthor)
   }

   @Transactional
   override fun partiaUpdateAuthor(id: Long, AuthorUpdate: AuthorUpdateRequest): AuthorEntity {
      val existingAuthor = authorRepository.findByIdOrNull(id)
      checkNotNull(existingAuthor)

      val updateAuthor = existingAuthor.com(
         name = authorUpdate.name ?: existingAuthor.name,
         age = authorUpdate.age ?: existingAuthor.age,
         description = authorUpdate.description ?: existingAuthor.description,
         image = authorUpdate.image ?: existingAuthor.image,
      )
      return authorRepository.save(updateAuthor)
   }

   
   override fun delete(id:Long){

   }
}
```
## Author Delete Test

①test/kotlin/controllers/AuthorControllerTest.kt編集する

```test/kotlin/controllers/AuthorControllerTest.kt
package com.devtiro.bookstore.controllers

import com.ninjasquad.springmockk.MockBean
import io.mockk.empty
import io.mockk.verify
import org.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.AutoConfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.post

private const val AUTHORS_BASE_URL = "/v1/authors"

@SpringBootTest
@AutoConfigureMockMvc
class AuthorControllerTest @Autowired constructor(
    private val mockMvc: MockMvc,
    @MockBean val authorService: AuthorService
    ) 

    val objectMapper = ObjectMapper()

    @BeforeEach
    fun beforeEach()
        every {
            authorService.save(any())
        } answers {
            firstArg()
    }

@Test
fun `test that create Author saves the Author`(){
    every {
        authorService.save(any())
    } answers {
        firstArg()
    }

    mockMvc.post("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(
            testAuthorDtoAuthorDto()
        )
    }
    val expected = AuthorEntity(
        id = null,
        name = "John Doe",
        age = 30,
        image = "author-image.jpeg",
        description = "some description"
    )
    verify{authorService.create(expected)}
}

@Test
fun `test that create Author returns a HTTP 201 status no a successful create`(){
    mockMvc.get("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(
            testAuthorDtoA()
        )
    }.andExpect {
        status { isCreated() }
    }
}

@Test
fun `test that create Author returns HTTP 400 when IllegalArgumentException is thrown`(){
every {
    authorService.create(any())
} throws (IllegalArgumentException())

    mockMvc.get("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(
            testAuthorDtoA()
        )
    }.andExpect {
        status { isBadRequest() }
    }
}

@Test
fun `test that list returns an empty list and HTTP 200 when no author in the database`(){
    every {
        authorService.list()
    } answers {
        enptyList()
}

    mockMvc.get("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
    } andExpect {
        status { isOk() }
        content { json ( "[]")}
}

@Test
fun `test that list returns author and HTTP 200 when author in the database`(){
    every {
        authorService.list()
    } answers {
        ListOf(testAuthorEntityA(1))
    }

    mockMvc.get("AUTHORS_BASE_URL"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
    }.andExpect {
        status { isOk() }
        content { jsonPath ( "$[0].id", equalTo(1))}
        content { jsonPath ( "$[0].name", equalTo("John Doe"))}
        content { jsonPath ( "$[0].age", equalTo(30))}
        content { jsonPath ( "$[0].description", equalTo("Some description"))}
        content { jsonPath ( "$[0].image", equalTo("author-image.jpeg"))}
    }

@Test
fun `test that get returns HTTP 404 when author in the database`(){
    every {
        authorService.get(any())
    } answers {
        null
    }

    mockMvc.get("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
    }}.andExpect {
        status { isNotFound() }
}

@Test
fun `test that get returns HTTP 200 and author when author found`(){
    every {
        authorService.get(any())
    } answers {
        testAuthorEntityA(id=999)
    }

    mockMvc.get("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
    }}.andExpect {
        status { isOk() }
        content { jsonPath ( "$.id", equalTo(999))}
        content { jsonPath ( "$.name", equalTo("John Doe"))}
        content { jsonPath ( "$.age", equalTo(30))}
        content { jsonPath ( "$.description", equalTo("Some description"))}
        content { jsonPath ( "$.image", equalTo("author-image.jpeg"))}
        }
    }

@Test
fun `test that full update Author return HTTP 200 and updated Author on successful call`(){
    every {
        authorService.fullUpdate(any(),any())
    } answers {
        secondArg()
    }
    
    mockMvc.put("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(testAuthorDtoA(id=999))
    }}.andExpect {
        status { isOk() }
        content { jsonPath ( "$.id", equalTo(999))}
        content { jsonPath ( "$.name", equalTo("John Doe"))}
        content { jsonPath ( "$.age", equalTo(30))}
        content { jsonPath ( "$.description", equalTo("Some description"))}
        content { jsonPath ( "$.image", equalTo("author-image.jpeg"))}
    }

@Test
fun `test that full update Author return HTTP 400 on IllegalStateException`(){
    every {
        authorService.fullUpdate(any(),any())
    } throws(IllegalStateException)
    
    mockMvc.put("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(testAuthorDtoA(id=999))
    }}.andExpect {
        status { isBadRequest() }
    }

@Test
fun `test that partial update Author return HTTP 400 on IllegalStateException`(){
    every {
        authorService.partialUpdate(any(),any())
    } throws(IllegalStateException)
    
    mockMvc.pacth("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(
            testAuthorUpdateRequestDtoA(id=999L)
        )
    } andExpect {
        status { isBadRequest()}
    }

@Test
fun `test that partial update Author return HTTP 200 and update Author`(){
    every {
        authorService.partialUpdate(any(),any())
    } answers {
        testAuthorEntityA(id=999)
    }

    mockMvc.pacth("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
        content = objectMapper.writeValueAsString(
            testAuthorUpdateRequestDtoA(id=999L)
        )
    } andExpect {
        status { isOk()}
        content { jsonPath ( "$.id", equalTo(999))}
        content { jsonPath ( "$.name", equalTo("John Doe"))}
        content { jsonPath ( "$.age", equalTo(30))}
        content { jsonPath ( "$.description", equalTo("Some description"))}
        content { jsonPath ( "$.image", equalTo("author-image.jpeg"))}   
    }
}

@Test
fun `test that delete Author returns HTTP 204 on successful delete`(){
    every {
        authorService.delete(any())
    } answers {}
    
    mockMvc.delete("${AUTHORS_BASE_URL}/999"){
        contentType = MediaType.APPLICATION_JSON
        accept = MediaType.APPLICATION_JSON
    } .andExpect {
        status { isNoContent()}
    }
  }
}

```
②test/kotlin/services/impl/AuthorServiceImplTest.ktを編集する

```test/kotlin/services/impl/AuthorServiceImplTest.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.testAuthorEntityA
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annoatation.Autowired
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
class AuthorServiceImplTest @Autowired constructor(
   private underTest: AuthorServiceImpl,
   private val authorRepository: authorRepository){

   @Test
   fun `test that save persists the Author in the database`(){
      val saveAuthor = underTest.save(testAuthorEntityA())
      assertThat(saveAuthor.id).isNotNull()

      val recalledAuthor = authorRepository.findById(saveAuthor.id)
      assertThat(recalledAuthor).isNotNull()
      assertThat(saveAuthor.id).isEqualTo(
      testAuthorEntityA(id=saveAuthor.id)
      )
   }

   @Test
   fun `test that an Author with an ID throws an IllegalArgumentException`(){
      assertThrows<IllegalArgumentException>{
         val existingAuthor = testAuthorEntityA(id=999)
         underTest.create(existingAuthor)
      }
   }

   @Test
   fun `test that list returns empty list when no author in the database`(){
      val result = underTest.list(testAuthorEntityA())
      assertThat(result).isEmpty()
   }

   @Test
   fun `test that list returns authors when present in the database`(){
      val saveAuthor = authorRepository.save(testAuthorEntityA())
      val expected = ListOf(saveAuthor)
      val result = underTest.list()
      assertThat(result).isEqualTo(expected)
   }

   @Test
   fun `test that get returns null when author not present in the database`(){
      val result = underTest.get(999)
      assertThat(result).isNull()
   }

   @Test
   fun `test that get returns author when author is present in the database`(){
      val saveAuthor = authorRepository.save(testAuthorEntityA())
      val result = underTest.get(saveAuthor.id)
      assertThat(result).isEqualTo(saveAuthor)
   }

   @Test
   fun `test that full update successfully update the author in the database`(){
      val existingAuthor = authorRepository.save(testAuthorEntityA())
      val existingAuthorId = existingAuthor.id
      val updateAuthorId = AuthorEntityB(
         id = existingAuthorId,
         name = "Don Joe",
         age = 45,
         description = "Some other description",
         image = "some-other-image.jpeg",
      )
      val result = underTest.fullUpdate(existingAuthorId, updateAuthor)
      assertThat(result).isEqualTo(updateAuthor)

      val retrievedAuthor = authorRepository.findByIdOrNull(existingAuthorId)
      assertThat(retrievedAuthor).isNotNull()
      assertThat(retrievedAuthor).isEqualTo(updateAuthor)
   }

   @Test
   fun `test that full update Author throws IllegalStateException when Author does not exist in the database`(){
      assertThrows<IllegalStateException> {
      val nonExistingAuthorId = 999L
      val updateAuthor = testAuthorEntityB(id=nonExistingAuthorId)
      underTest.fullUpdate(nonExistingAuthorIdm updateAuthor)
      }
   }

   @Test
   fun `test that partial update Author throws IllegalStateException when Author does not exist in the database`(){
      assertThrows<IllegalStateException> {
      val nonExistingAuthorId = 999L
      val updateRequest = testAuthorUpdateRequestA(id=nonExistingAuthorId)
      underTest.partialUpdate(nonExistingAuthorId, updateAuthor)
  }
}

   @Test
   fun `test that partial update Author throws does not update Author when all values exist are null`(){
      val existingAuthor = authorRepository.save(testAuthorEntityA())
      val updateAuthor = underTest.partialUpdate(existingAuthor.id!!, AuthorUpdateRequest())
      assertThat(updateAuthor).isEqualTo(existingAuthor)
  }
   @Test
   fun `test that partial update Author updates author name`(){
      val newName = "New Name"
      val AuthorUpdateRequest = authorUpdateRequest(
         name = newName
      )
      
      val AuthorUpdateRequest = authorUpdateRequest(
         name = newName
      )
      assertThatAuthorPartialUpdateIsUpdaed(
      existingAuthor = existingAuthor,
      expectedAuthor = expectedAuthor,
      authorUpdateRequest = authorUpdateRequest
      )
   }

   private fun assertThatAuthorPartialUpdateIsUpdaed(
      existingAuthor: AuthorEntity,
      expectedAuthor: AuthorEntity,
      authorUpdate: AuthorUpdateRequest
   ) {
      val savedExistingAuthor = authorRepository.save(existingAuthor)
      val existingAuthorId = existingAuthor.id!!
      val updateAuthor = underTest.partialUpdate(
         existingAuthorsId, AuthorUpdateRequest(
      ))
      val expected = expectedAuthor.copy(id=existingAuthorId)
      assertThat(updatedAuthor).isEqualTo(expected)

      val retrievedAuthor = authorRepository.findByIdOrNull(existingAuthorId)
      assertThat(retrievedAuthor).isNotNull()

      assertThat(updatedAuthor).isEqualTo(expected)
   }
}

@Test
fun `test that partial update Author updates author age`(){
   val newAge = "New Age"
   val existingAuthor = existingAuthorEntityA()
   val expectedAuthor = existingAuthor.copy(
         age = newAge
      )
      val AuthorUpdateRequest = authorUpdateRequest(
         age = newAge
      )
      assertThatAuthorPartialUpdateIsUpdaed(
         existingAuthor = existingAuthor,
         expectedAuthor = expectedAuthor,
         authorUpdateRequest = authorUpdateRequest,
      )
}

@Test
fun `test that partial update Author updates author description`(){
   val newDescription = "A new description"
   val existingAuthor = existingAuthorEntityA()
   val expectedAuthor = existingAuthor.copy(
         description = newDescription
      )
      val AuthorUpdateRequest = authorUpdateRequest(
         description = newDescription
      )
      assertThatAuthorPartialUpdateIsUpdaed(
         existingAuthor = existingAuthor,
         expectedAuthor = expectedAuthor,
         authorUpdateRequest = authorUpdateRequest,
      )
}

@Test
fun `test that partial update Author updates author image`(){
   val newImage = "A new image"
   val existingAuthor = existingAuthorEntityA()
   val expectedAuthor = existingAuthor.copy(
         image = newImage
      )
      val AuthorUpdateRequest = authorUpdateRequest(
         image = "newImage"
      )
      assertThatAuthorPartialUpdateIsUpdaed(
         existingAuthor = existingAuthor,
         expectedAuthor = expectedAuthor,
         authorUpdateRequest = authorUpdateRequest,
      )
   

   private fun assertThatAuthorPartialUpdateIsUpdaed (
      
         existingAuthor: AuthorEntity,
         authorUpdateRequest: AuthorUpdateRequest
   ){

      val savedExistingAuthor = authorRepository.save(existingAuthor)
      val existingAuthorsId = savedExistingAuthor.id!!

      val updateAuthor = underTest.partialUpdate(
         existingAuthorId, authorUpdateRequest)

      val expected = expectedAuthor.copy(id=existingAuthorId)
      assertThat(updateAuthor).isEqualTo(expected)

      val retrievedAuthor = authorRepository.findByIdOrNull(existingAuthorId)
      assertThat(retrievedAuthor).isNotNull()
      assertThat(retrievedAuthor).isEqualTo(expected)
}

@Test
fun `test that partial update Author updates author image`(){
   val existingAuthor = authorRepository.save(existingAuthorEntityA())
   val existingAuthorId = existingAuthor.id!!

   underTest.delete(existingAuthorsId)

   assertThat(
      authorRepository.existById(existingAuthorsId)
   ).isFalse()
 }

 @Test
fun `test that delete deletes an non-existing Author in the database`(){
   
   val nonExistingId = 999L
   underTest.delete(nonExistingId)

   assertThat(
      authorRepository.existById(nonExistingId)
   ).isFalse()
 }
}
```

## Test Pollution

①test/kotlin/services/impl/AuthorServiceImplTest.ktを編集する

```test/kotlin/services/impl/AuthorServiceImplTest.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.testAuthorEntityA
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.springframework.beans.factory.annoatation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.database.repository.findByIdOrNull

@SpringBootTest
@Transactional
class AuthorServiceImplTest @Autowired constructor(
   private underTest: AuthorServiceImpl,
   private val authorRepository: authorRepository){

   @Test
   fun `test that save persists the Author in the database`(){
      val saveAuthor = underTest.save(testAuthorEntityA())
      assertThat(saveAuthor.id).isNotNull()

      val recalledAuthor = authorRepository.findById(saveAuthor.id)
      assertThat(recalledAuthor).isNotNull()
      assertThat(saveAuthor.id).isEqualTo(
      testAuthorEntityA(id=saveAuthor.id)
      )
   }

   @Test
   fun `test that an Author with an ID throws an IllegalArgumentException`(){
      assertThrows<IllegalArgumentException>{
         val existingAuthor = testAuthorEntityA(id=999)
         underTest.create(existingAuthor)
      }
   }

   @Test
   fun `test that list returns empty list when no author in the database`(){
      val result = underTest.list(testAuthorEntityA())
      assertThat(result).isEmpty()
   }

   @Test
   fun `test that list returns authors when present in the database`(){
      val saveAuthor = authorRepository.save(testAuthorEntityA())
      val expected = ListOf(saveAuthor)
      val result = underTest.list()
      assertThat(result).isEqualTo(expected)
   }

   @Test
   fun `test that get returns null when author not present in the database`(){
      val result = underTest.get(999)
      assertThat(result).isNull()
   }

   @Test
   fun `test that get returns author when author is present in the database`(){
      val saveAuthor = authorRepository.save(testAuthorEntityA())
      val result = underTest.get(saveAuthor.id)
      assertThat(result).isEqualTo(saveAuthor)
   }

   @Test
   fun `test that full update successfully update the author in the database`(){
      val existingAuthor = authorRepository.save(testAuthorEntityA())
      val existingAuthorId = existingAuthor.id
      val updateAuthorId = AuthorEntityB(
         id = existingAuthorId,
         name = "Don Joe",
         age = 45,
         description = "Some other description",
         image = "some-other-image.jpeg",
      )
      val result = underTest.fullUpdate(existingAuthorId, updateAuthor)
      assertThat(result).isEqualTo(updateAuthor)

      val retrievedAuthor = authorRepository.findByIdOrNull(existingAuthorId)
      assertThat(retrievedAuthor).isNotNull()
      assertThat(retrievedAuthor).isEqualTo(updateAuthor)
   }

   @Test
   fun `test that full update Author throws IllegalStateException when Author does not exist in the database`(){
      assertThrows<IllegalStateException> {
      val nonExistingAuthorId = 999L
      val updateAuthor = testAuthorEntityB(id=nonExistingAuthorId)
      underTest.fullUpdate(nonExistingAuthorIdm updateAuthor)
      }
   }

   @Test
   fun `test that partial update Author throws IllegalStateException when Author does not exist in the database`(){
      assertThrows<IllegalStateException> {
      val nonExistingAuthorId = 999L
      val updateRequest = testAuthorUpdateRequestA(id=nonExistingAuthorId)
      underTest.partialUpdate(nonExistingAuthorId, updateAuthor)
  }
}

   @Test
   fun `test that partial update Author throws does not update Author when all values exist are null`(){
      val existingAuthor = authorRepository.save(testAuthorEntityA())
      val updateAuthor = underTest.partialUpdate(existingAuthor.id!!, AuthorUpdateRequest())
      assertThat(updateAuthor).isEqualTo(existingAuthor)
  }
   @Test
   fun `test that partial update Author updates author name`(){
      val newName = "New Name"
      val AuthorUpdateRequest = authorUpdateRequest(
         name = newName
      )
      
      val AuthorUpdateRequest = authorUpdateRequest(
         name = newName
      )
      assertThatAuthorPartialUpdateIsUpdaed(
      existingAuthor = existingAuthor,
      expectedAuthor = expectedAuthor,
      authorUpdateRequest = authorUpdateRequest
      )
   }

   private fun assertThatAuthorPartialUpdateIsUpdaed(
      existingAuthor: AuthorEntity,
      expectedAuthor: AuthorEntity,
      authorUpdate: AuthorUpdateRequest
   ) {
      val savedExistingAuthor = authorRepository.save(existingAuthor)
      val existingAuthorId = existingAuthor.id!!
      val updateAuthor = underTest.partialUpdate(
         existingAuthorsId, AuthorUpdateRequest(
      ))
      val expected = expectedAuthor.copy(id=existingAuthorId)
      assertThat(updatedAuthor).isEqualTo(expected)

      val retrievedAuthor = authorRepository.findByIdOrNull(existingAuthorId)
      assertThat(retrievedAuthor).isNotNull()

      assertThat(updatedAuthor).isEqualTo(expected)
   }
}

@Test
fun `test that partial update Author updates author age`(){
   val newAge = "New Age"
   val existingAuthor = existingAuthorEntityA()
   val expectedAuthor = existingAuthor.copy(
         age = newAge
      )
      val AuthorUpdateRequest = authorUpdateRequest(
         age = newAge
      )
      assertThatAuthorPartialUpdateIsUpdaed(
         existingAuthor = existingAuthor,
         expectedAuthor = expectedAuthor,
         authorUpdateRequest = authorUpdateRequest,
      )
}

@Test
fun `test that partial update Author updates author description`(){
   val newDescription = "A new description"
   val existingAuthor = existingAuthorEntityA()
   val expectedAuthor = existingAuthor.copy(
         description = newDescription
      )
      val AuthorUpdateRequest = authorUpdateRequest(
         description = newDescription
      )
      assertThatAuthorPartialUpdateIsUpdaed(
         existingAuthor = existingAuthor,
         expectedAuthor = expectedAuthor,
         authorUpdateRequest = authorUpdateRequest,
      )
}

@Test
fun `test that partial update Author updates author image`(){
   val newImage = "A new image"
   val existingAuthor = existingAuthorEntityA()
   val expectedAuthor = existingAuthor.copy(
         image = newImage
      )
      val AuthorUpdateRequest = authorUpdateRequest(
         image = "newImage"
      )
      assertThatAuthorPartialUpdateIsUpdaed(
         existingAuthor = existingAuthor,
         expectedAuthor = expectedAuthor,
         authorUpdateRequest = authorUpdateRequest,
      )
   

   private fun assertThatAuthorPartialUpdateIsUpdaed (
      
         existingAuthor: AuthorEntity,
         authorUpdateRequest: AuthorUpdateRequest
   ){

      val savedExistingAuthor = authorRepository.save(existingAuthor)
      val existingAuthorsId = savedExistingAuthor.id!!

      val updateAuthor = underTest.partialUpdate(
         existingAuthorId, authorUpdateRequest)

      val expected = expectedAuthor.copy(id=existingAuthorId)
      assertThat(updateAuthor).isEqualTo(expected)

      val retrievedAuthor = authorRepository.findByIdOrNull(existingAuthorId)
      assertThat(retrievedAuthor).isNotNull()
      assertThat(retrievedAuthor).isEqualTo(expected)
}

@Test
fun `test that partial update Author updates author image`(){
   val existingAuthor = authorRepository.save(existingAuthorEntityA())
   val existingAuthorId = existingAuthor.id!!

   underTest.delete(existingAuthorsId)

   assertThat(
      authorRepository.existById(existingAuthorsId)
   ).isFalse()
 }

 @Test
fun `test that delete deletes an non-existing Author in the database`(){
   
   val nonExistingId = 999L
   underTest.delete(nonExistingId)

   assertThat(
      authorRepository.existById(nonExistingId)
   ).isFalse()
 }
}
```
## Book Create Update

①controllers/BooksController.ktを作成する

```controllers/BooksController.kt
package com.devtiro.bookstore.controllers

import com.devtiro.bookstore.domain.dto.BookSummaryDto
import com.devtiro.bookstore.services.BookService
import org.springframework.web.bind.annoatation.*

@RestController
class BooksController(val BookService: BookService) {

    @PutMapping(path = ["/v1/books/{isbn}"])
    fun createFullUpdateBook(@PathVariable("isbn")isbn: String, @RequestBody book: BookSummaryDto): responseEntity<BookSummaryDto>{
        try {
          val (savedBook, siCreated) = BookService.createUpdate(isbn, book.toBookSummary())
          val responseCode = if(isCreated) HttpStatus.CREATED else HttpStatus.OK
          return ResponseEntity(saveBook.toBookSummaryDto(), responseCode)

      } catch(ex: IllegalStateException){
          return ResponseEntity(HttpStatus.INTENAL_SERVER_ERROR)
      } catch(ex: IllegalStateException){
        return ResponseEntity(HttpStatus.BAD_REQUEST)
    }
  }
}
```

②domain/dto/AuthorSummaryDto.ktを作成する

```domain/dto/AuthorSummaryDto.kt
package com.devtiro.bookstore.domain.dto

data class AuthorSummaryDto(
    val id: Long,
    val name: String?,
    val image: String?
)
```

③domain/dto/BookSummaryDto.ktを作成する

```domain/dto/BookSummaryDto.kt
package com.devtiro.bookstore.domain.dto

data class BookSummaryDto(
    var isbn: String,
    var title: String,
    var description: String,
    var image: String,
    var author: AuthorSummaryDto
)
```
④services/BookService.ktを作成する

```services/BookService.kt
package com.devtiro.bookstore.services

import com.devtiro.bookstore.domain.BookSummary

interface BookService {

    fun createUpdate(isbn: String, bookSummary: BookSummary):Pair<BookEntity, Boolean>
}
```
⑤test/kotlin/services/impl/BookServiceImpl.ktを作成する

```test/kotlin/services/impl/BookServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.domain.BookSummary
import com.devtiro.bookstore.domain.entities.BookEntity
import com.devtiro.bookstore.repository.AuthorRepository
import com.devtiro.bookstore.repository.BookRepository
import com.devtiro.bookstore.services.BookService
import com.devtiro.bookstore.toBookEntity
import org.springframework.data.repository.findByIdOrNull


@Services
class BookServiceImpl(
    val bookRepository: bookRepository,
    val authorRepository: AuthorRepository,) :BookService {

    @Transactional
    override fun createUpdate(isbn: String, bookSummary: BookSummary): Pair<BookEntity, Boolean> {
        val normalisedBook = bookSummary.copy(isbn = isbn)
        val isExists = bookRepository.existsById(isbn)

        val author = authorRepository.findByIdOrNull(normalisedBook.author.id)
        checkNotNull(author)

        val saveBook = bookRepository.save(normalisedBook.toBookEntity(author))
        return Pair(saveBook, !isExists)
    }

}
```
⑥kotlin/com/devtiro/bookstore/Extensions.ktを編集する

```kotlin/com/devtiro/bookstore/Extensions.kt
package com.devtiro.bookstore

import com.devtiro.bookstore.domain.dto.AuthorDto
import com.devtiro.bookstore.domain.dto.BookSummaryDto
import com.devtiro.bookstore.domain.entities.AuthorEntity
import com.devtiro.bookstore.domain.entities.BookEntity

fun AuthorEntity.toAuthorDto() = AuthorDto(
    id=this.id,
    name=this.name,
    age=this.age,
    description=this.description,
    image=this.image
)

fun AuthorEntity.toAuthorSummaryDto(): AuthorSummaryDto{
    val authorId = this.id ?:throw InvalidAuthorException()
    checkNotNull(authorId)
    return AuthorSummaryDto(
        id=this.id,
        name=this.name,
        image=this.image
    )
}

fun AuthorDto.toAuthorEntity() = AuthorEntity(
    id=this.id,
    name=this.name,
    age=this.age,
    description=this.description,
    image=this.image
)

fun AuthorUpdateRequestDto.toAuthorUpdateRequestDto() = AuthorUpdateRequest(
    id=this.id,
    name=this.name,
    age=this.age,
    description=this.description,
    image=this.image
)

fun BookSummary.toBookEntity(author: AuthorEntity) = BookEntity(
    isbn=this.isbn,
    title=this.title,
    description=this.description,
    image=this.image,
    authorEntity=this.authorEntity
)

fun AuthorSummaryDto.toAuthorSummary() = AuthorSummary(
    id=this.id,
    name=this.name,
    image=this.image
)

fun BookSummaryDto.toBookEntity(author: AuthorEntity) = BookSummary(
    isbn=this.isbn,
    title=this.title,
    description=this.description,
    image=this.image,
    author=this.author.toAuthorSummary()
)

fun BookEntity.toBookSummaryDto() = BookSummaryDto(
    isbn=this.isbn,
    title=this.title,
    description=this.description,
    image=this.image,
    author=this.authorEntityDto()
)
```
⑦domain/entities/BookEntity.ktを編集する

```domain/entities/BookEntity.kt
package com.devtiro.bookstore.domain

import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.Table

@Entity
@Table(name="books")
data class BookEntity (
    @Id
    @Column(name="isbn")
    var isbn: String,

    @Column(name="title")
    var title: String,

    @Column(name="description")
    var description: String,

    @Column(name="image")
    var image: String,

    @ManyToOne(cascade=[CascadeType.DETACH])
    @JoinColumn(name="author_id")
    var authorEntity: AuthorEntity
    )
```
⑧domain/exceptionsptions/InvalidAuthor.ktを作成する

```domain/exceptionsptions/InvalidAuthor.kt
package com.devtiro.bookstore.exception

class InvalidAuthorException: Exception()
```

## Book Create Update Test

①test/kotlin/controllers/BooksControllerTest.ktを作成する

```test/kotlin/controllers/BooksControllerTest.kt
package com.devtiro.bookstore.controllers

import com.devtiro.bookstore.services.AuthorService
import com.devtiro.bookstore.services.BookService
import com.festerxml.jackson.databind.ObjectMapper
import com.ninjasquad.springmockk.MockBean
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.AutoConfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.put
import org.springframework.test.web.servlet.result.StatusResultMatchersDsl


@SpringBootTest
@AutoConfigureMockMvc
class BooksControllerTest @Autowired constructor　(
    private val mockMvc: MockMvc,
    @MockkBean val authorService: AuthorService
    ) {
    val objectMapper = ObjectMapper()

    @Test
    fun `test that createFullUpdateBook return HTTP 201 when book is created`(){
        assertThatUserCreatedUpdated(true){isCreated()}
    }

    @Test
    fun `test that createFullUpdateBook return HTTP 200 when book is updated`(){
        assertThatUserCreatedUpdated(false){isOk()}
    }

    private fun assertThatUserCreatedUpdated(isCreated: Boolean, statusCodeAssertion: StatusResultMatchersDsl.() -> Unit) {
        val isbn = "978-089-230342-0777"
        val author = testAuthorEntityA(id=1)
        val savedBook = testBookEntityA(isbn, author)

        val authorSummaryDto = testBookSummaryDtoA(id=1)
        val bookSummaryDto = testBookSummaryDtoA(isbn, authorSummaryDto)

        every {
            bookService.createUpdate(isbn, any())
        } answers {
            Pair(savedBook, isCreated)
        }

        mockMvc.put("/v1/books/${isbn}"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookSummaryDto)
        }.andExpect {
            status { statusCodeAssertion()}
        }
    }


    @Test
    fun `test that createFullUpdateBook returns HTTP 500 when author in the database does not have an ID`(){
        val isbn = "978-089-230342-0777"
        val author = testAuthorEntityA(id=1)
        val savedBook = testBookEntityA(isbn, author)

        val authorSummaryDto = testBookSummaryDtoA(id=1)
        val bookSummaryDto = testBookSummaryDtoA(isbn, authorSummaryDto)

        every {
            bookService.createUpdate(isbn, any())
        } answers {
            Pair(savedBook, true)
        }

        mockMvc.put("/v1/books/${isbn}"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookSummaryDto)
        }.andExpect {
            status { isInternalServerError()}
        }
    }


    @Test
    fun `test that createFullUpdateBook returns HTTP 400 when author does not exist`(){
        val isbn = "978-089-230342-0777"
        
        val authorSummaryDto = testBookSummaryDtoA(id=1)
        val bookSummaryDto = testBookSummaryDtoA(isbn, authorSummaryDto)

        every {
            bookService.createUpdate(isbn, any())
        } throws IllegalStateException()

        mockMvc.put("/v1/books/${isbn}"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookSummaryDto)
        }.andExpect {
            status { isBadRequest()}
        }
    }
}
```
②test/kotlin/services/impl/TestDataUtil.ktを編集する

```test/kotlin/services/impl/TestDataUtil.kt
package com.devtiro.bookstore

import com.devtiro.bookstore.domain.AuthorUpdateRequest
import com.devtiro.bookstore.domain.dto.AuthorDto
import com.devtiro.bookstore.domain.dto.AuthorSummaryDto
import com.devtiro.bookstore.domain.dto.AuthorUpdateRequestDto
import com.devtiro.bookstore.domain.dto.BookSummaryDto
import com.devtiro.bookstore.domain.entities.AuthorEntity
import com.devtiro.bookstore.domain.entities.BookEntity

const val BOOK_A_ISBN = "978-089-230342-0777"

fun testAuthorDtoA(id: Long? = null) = AuthorDto(
    id = id,
    name = "John Doe",
    age = 30,
    description = "some description"
    image = "author-image.jpeg",
)

fun testAuthorEntityA(id: Long? = null) = AuthorEntity(
    id = id,
    name = "John Doe",
    age = 30,
    description = "some description"
    image = "author-image.jpeg",
)

fun testAuthorEntityB(id: Long? = null) = AuthorEntity(
    id = null,
    name = "John Doe",
    age = 65,
    description = "some other description"
    image = "some-other-image.jpeg",
)

fun testAuthorSummaryDtoA(id: Long) = AuthorSummaryDto(
    id = id,
    name = "John Doe",
    image = "author-image.jpeg",
)

fun testAuthorSummaryA(id: Long) = AuthorSummary(
    id = id,
    name = "John Doe",
    image = "author-image.jpeg",
)

fun testAuthorUpdateRequestDtoA(id: Long? = null) = AuthorUpdateRequestDto(
    id = null,
    name = "John Doe",
    age = 30,
    description = "some description"
    image = "author-image.jpeg",
)

fun testAuthorUpdateRequestA(id: Long? = null) = AuthorUpdateRequest(
    id = null,
    name = "John Doe",
    age = 30,
    description = "some description"
    image = "author-image.jpeg",
)

fun testBookEntityA(isbn: String, author: AuthorEntity) = BookEntity(
    isbn = isbn,
    title = "Test Book A",
    description = "A test book"
    image = "book-image.jpeg",
    authorEntity = author
)

fun testBookSummaryDtoA(isbn: String, author: AuthorSummaryDto) = BookSummaryDto(
    isbn = isbn,
    title = "Test Book A",
    description = "A test book"
    image = "book-image.jpeg",
    author = author
)

fun testBookSummaryB(isbn: String, author: AuthorSummary) = BookSummary(
    isbn = isbn,
    title = "Test Book B",
    description = "Author test book"
    image = "book-image-b.jpeg",
    author = author
)
```
③test/kotlin/services/impl/BookServiceImplTest.ktを作成する

```test/kotlin/services/impl/BookServiceImplTest.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.repositories.AuthorRepository
import com.devtiro.bookstore.repositories.BookRepository
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.transaction.annotation.Transactional
import org.junit.jupiter.api.assertThrows
import java.lang.IllegalArgumentException
import org.springframework.boot.test.context.Transactional
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annoatation.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
@Transactional
class BookServiceImplTest @Autowired constructor(

    private val underTest: AuthorServiceImpl,
    private val bookRepository: BookRepository,
    private val authorRepository: AuthorRepository
){

    @Test
    fun `test that createUpdate throws IllegalStateException when Author does not exist`(){
        val authorSummary = AuthorSummary(id=999L)
        val bookRepository = testBookSummaryA(BOOK_A_ISBN, authorSummary)
        assertThrows<IllegalStateException> {
            underTest.createUpdate(BOOK_A_ISBN, bookRepository)
        }
    }

    @Test
    fun `test that createUpdate throws successfully creates book in the database`(){
        val saveDAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val authorSummary = AuthorSummary(id=savedAuthor!!)
        val bookRepository = testBookSummaryA(BOOK_A_ISBN, authorSummary)
        val (savedBook, isCreated) = underTest.createUpdate(BOOK_A_ISBN, bookRepository)
        assertThat(savedBook).isNotNull()

        val recalledBook = bookRepository.findByIdOrNull(BOOK_A_ISBN)
        assertThat(recalledBook).isNotNull()
        assertThat(recalledBook).isEqualTo(savedBook)
        assertThat(isCreated).isTrue()
    }

    @Test
    fun `test that createUpdate throws successfully update book in the database`(){
        val saveDAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, saveAuthor))
        assertThat(savedBook).isNotNull()

        val authorSummary = AuthorSummary(id=savedAuthor!!)
        val bookRepository = testBookSummaryA(BOOK_A_ISBN, authorSummary)
        val (ipdateBook, isCreated) = underTest.createUpdate(BOOK_A_ISBN, bookRepository)
        assertThat(savedBook).isNotNull()

        val recalledBook = bookRepository.findByIdOrNull(BOOK_A_ISBN)
        assertThat(recalledBook).isNotNull()
        assertThat(isCreated).isFalse()
    }
}

```
## Book Read Many

①controllers/BooksController.ktを編集する

```controllers/BooksController.kt
package com.devtiro.bookstore.controllers

import com.devtiro.bookstore.services.BookService
import com.devtiro.bookstore.domain.dto.toBookSummary
import com.devtiro.bookstore.domain.dto.toBookSummaryDto
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annoatation.*

@RestController
class BooksController(val BookService: BookService) {

    @PutMapping(path = ["/v1/books/{isbn}"])
    fun createFullUpdateBook(@PathVariable("isbn")isbn: String, @RequestBody book: BookSummaryDto): responseEntity<BookSummaryDto>{
        try {
          val (savedBook, siCreated) = BookService.createUpdate(isbn, book.toBookSummary())
          val responseCode = if(isCreated) HttpStatus.CREATED else HttpStatus.OK
          return ResponseEntity(saveBook.toBookSummaryDto(), responseCode)

      } catch(ex: IllegalStateException){
          return ResponseEntity(HttpStatus.INTENAL_SERVER_ERROR)
      } catch(ex: IllegalStateException){
          return ResponseEntity(HttpStatus.BAD_REQUEST)
    }
  }

  @GetMapping(path = ["/v1/books"])
  fun readManyBooks(): List<BookSummaryDto> {
      return BookService.list().map { it.toBookSummaryDto()}
  }
}
```
②services/BooKService.ktを編集する

```services/BooKService.kt
package com.devtiro.bookstore.services

import com.devtiro.bookstore.domain.BookSummary
import com.devtiro.bookstore.domain.entity.BookEntity

interface BookService {

    fun createUpdate(isbn: String, bookSummary: BookSummary):Pair<BookEntity, Boolean>

    fun list(): List<BookEntity>
}
```

③services/impl/BookServiceImpl.ktを作成する

```ervices/impl/BookServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.domain.BookSummary
import com.devtiro.bookstore.domain.entities.BookEntity
import com.devtiro.bookstore.repository.AuthorRepository
import com.devtiro.bookstore.repository.BookRepository
import com.devtiro.bookstore.services.BookService
import com.devtiro.bookstore.toBookEntity
import org.springframework.data.repository.findByIdOrNull


@Services
class BookServiceImpl(
    val bookRepository: bookRepository,
    val authorRepository: AuthorRepository,) :BookService {

    @Transactional
    override fun createUpdate(isbn: String, bookSummary: BookSummary): Pair<BookEntity, Boolean> {
        val normalisedBook = bookSummary.copy(isbn = isbn)
        val isExists = bookRepository.existsById(isbn)

        val author = authorRepository.findByIdOrNull(normalisedBook.author.id)
        checkNotNull(author)

        val saveBook = bookRepository.save(normalisedBook.toBookEntity(author))
        return Pair(saveBook, !isExists)
    }

    override fun list(): List<BookEntity> {
        return bookRepository.findAll()
    }
}
```
## Fix Cascade Issues

①resources/db.migration/V1_int_db/sqlを作成する

```resources/db.migration/V1_int_db.sql
DROP SEQUENCE IF EXISTS "authors";
CREATE SEQUENCE "authors" INCREMENT BY 50 START WITH 1;

DROP SEQUENCE IF EXISTS "authors";
CREATE TABLE "authors" (
    "id" bigint NOT NULL,
    "age" smallint,
    "description" VARCHAR(512),
    "image" VARCHAR(512),
    "name" VARCHAR(512),
    CONSTRAINT "authors_pkey" PRIMARY KEY("id")
);

DROP SEQUENCE IF EXISTS "books";
CREATE TABLE "books" (
    "isbn" VARCHAR(19) NOT NULL,
    "description" VARCHAR(2048),
    "image" VARCHAR(512),
    "title" VARCHAR(512),
    "author_id" bigint NOT NULL REFERENCES "author" ("id"),
    CONSTRAINT "books_pkey" PRIMARY KEY("isbn")
);
```
①domain/entities/AuthorEntity.ktを編集する

```domain/entities/AuthorEntity.kt
package com.devtiro.bookstore.domain

import jakarta.persistence.*

@Entity
@Table(name="author")
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "author_id_seq")
data class Author (
    @Id
    @Column(name="id")
    val id: Long?,

    @Column(name="name")
    val name: String,

    @Column(name="age")
    val age: Int,

    @Column(name="description")
    val description: String,

    @Column(name="image")
    val image: String,

    @ManyToOne(mappedBy = "authorEntity", cascade=[CascadeType.REMOVE])
    val BookEntities: List<BookEntity> = emptyList(),
)
```
## Test Read Many Books

①test/kotlin/controllers/BooksControllerTest.ktを編集する

```test/kotlin/controllers/BooksControllerTest.kt
package com.devtiro.bookstore.controllers

import com.devtiro.bookstore.services.AuthorService
import com.devtiro.bookstore.services.BookService
import com.festerxml.jackson.databind.ObjectMapper
import com.ninjasquad.springmockk.MockBean
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.AutoConfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.put
import org.springframework.test.web.servlet.result.StatusResultMatchersDsl


@SpringBootTest
@AutoConfigureMockMvc
class BooksControllerTest @Autowired constructor　(
    private val mockMvc: MockMvc,
    @MockkBean val authorService: AuthorService
    ) {
    val objectMapper = ObjectMapper()

    @Test
    fun `test that createFullUpdateBook return HTTP 201 when book is created`(){
        assertThatUserCreatedUpdated(true){isCreated()}
    }

    @Test
    fun `test that createFullUpdateBook return HTTP 200 when book is updated`(){
        assertThatUserCreatedUpdated(false){isOk()}
    }

    private fun assertThatUserCreatedUpdated(isCreated: Boolean, statusCodeAssertion: StatusResultMatchersDsl.() -> Unit) {
        val isbn = "978-089-230342-0777"
        val author = testAuthorEntityA(id=1)
        val savedBook = testBookEntityA(isbn, author)

        val authorSummaryDto = testBookSummaryDtoA(id=1)
        val bookSummaryDto = testBookSummaryDtoA(isbn, authorSummaryDto)

        every {
            bookService.createUpdate(isbn, any())
        } answers {
            Pair(savedBook, isCreated)
        }

        mockMvc.put("/v1/books/${isbn}"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookSummaryDto)
        }.andExpect {
            status { statusCodeAssertion()}
        }
    }


    @Test
    fun `test that createFullUpdateBook returns HTTP 500 when author in the database does not have an ID`(){
        val isbn = "978-089-230342-0777"
        val author = testAuthorEntityA(id=1)
        val savedBook = testBookEntityA(isbn, author)

        val authorSummaryDto = testBookSummaryDtoA(id=1)
        val bookSummaryDto = testBookSummaryDtoA(isbn, authorSummaryDto)

        every {
            bookService.createUpdate(isbn, any())
        } answers {
            Pair(savedBook, true)
        }

        mockMvc.put("/v1/books/${isbn}"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookSummaryDto)
        }.andExpect {
            status { isInternalServerError()}
        }
    }


    @Test
    fun `test that createFullUpdateBook returns HTTP 400 when author does not exist`(){
        val isbn = "978-089-230342-0777"
        
        val authorSummaryDto = testBookSummaryDtoA(id=1)
        val bookSummaryDto = testBookSummaryDtoA(isbn, authorSummaryDto)

        every {
            bookService.createUpdate(isbn, any())
        } throws IllegalStateException()

        mockMvc.put("/v1/books/${isbn}"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookSummaryDto)
        }.andExpect {
            status { isBadRequest()}
        }
    }

    @Test
    fun `test that readManyBooks returns a list of books`(){
        val isbn = "978-089-230342-0777"
        
        every {
            bookService.list()
        } answers {
            listOf(testAuthorEntityA(isbn = isbn, testAuthorEntityA(id=1)))
        }
        
        mockMvc.get("/v1/books"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk()}
            content { jsonPath ( "$[0].isbn", equalTo(isbn))}
            content { jsonPath ( "$[0].title", equalTo(isbn))}
            content { jsonPath ( "$[0].image", equalTo("book-image.jpeg"))}
            content { jsonPath ( "$[0].author.id", equalTo(1))}
            content { jsonPath ( "$[0].author.name", equalTo("John Doe"))}
            content { jsonPath ( "$[0].author.image", equalTo("author-image.jpeg"))}
        }
    }
}
```

②test/kotlin/services/impl/BookServiceImplTest.ktを作成する

```test/kotlin/services/impl/BookServiceImplTest.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.repositories.AuthorRepository
import com.devtiro.bookstore.repositories.BookRepository
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.transaction.annotation.Transactional
import org.junit.jupiter.api.assertThrows
import java.lang.IllegalArgumentException
import org.springframework.boot.test.context.Transactional
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annoatation.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
@Transactional
class BookServiceImplTest @Autowired constructor(

    private val underTest: AuthorServiceImpl,
    private val bookRepository: BookRepository,
    private val authorRepository: AuthorRepository
){

    @Test
    fun `test that createUpdate throws IllegalStateException when Author does not exist`(){
        val authorSummary = AuthorSummary(id=999L)
        val bookRepository = testBookSummaryA(BOOK_A_ISBN, authorSummary)
        assertThrows<IllegalStateException> {
            underTest.createUpdate(BOOK_A_ISBN, bookRepository)
        }
    }

    @Test
    fun `test that createUpdate throws successfully creates book in the database`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val authorSummary = AuthorSummary(id=savedAuthor!!)
        val bookRepository = testBookSummaryA(BOOK_A_ISBN, authorSummary)
        val (savedBook, isCreated) = underTest.createUpdate(BOOK_A_ISBN, bookRepository)
        assertThat(savedBook).isNotNull()

        val recalledBook = bookRepository.findByIdOrNull(BOOK_A_ISBN)
        assertThat(recalledBook).isNotNull()
        assertThat(recalledBook).isEqualTo(savedBook)
        assertThat(isCreated).isTrue()
    }

    @Test
    fun `test that createUpdate throws successfully update book in the database`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, saveAuthor))
        assertThat(savedBook).isNotNull()

        val authorSummary = AuthorSummary(id=savedAuthor!!)
        val bookRepository = testBookSummaryA(BOOK_A_ISBN, authorSummary)
        val (ipdateBook, isCreated) = underTest.createUpdate(BOOK_A_ISBN, bookRepository)
        assertThat(savedBook).isNotNull()

        val recalledBook = bookRepository.findByIdOrNull(BOOK_A_ISBN)
        assertThat(recalledBook).isNotNull()
        assertThat(isCreated).isFalse()
    }

    @Test
    fun `test that list returns an empty list when no book in the database`(){
        val result = underTest.list()
        assertThat(result).isEmpty()
    }

    @Test
    fun `test that list returns books list when no book in the database`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, savedAuthor))
        assertThat(savedBook).isNotNull()

        val result = underTest.list()
        assertThat(result).hasSize(1)
        assertThat(result.get[0]).isEqualTo(savedAuthor)
    }

}
```

## Read Many Books Query

①controllers/BooksController.ktを編集する

```controllers/BooksController.kt
package com.devtiro.bookstore.controllers

import com.devtiro.bookstore.services.BookService
import com.devtiro.bookstore.domain.dto.toBookSummary
import com.devtiro.bookstore.domain.dto.toBookSummaryDto
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annoatation.*

@RestController
class BooksController(val BookService: BookService) {

    @PutMapping(path = ["/v1/books/{isbn}"])
    fun createFullUpdateBook(@PathVariable("isbn")isbn: String, @RequestBody book: BookSummaryDto): responseEntity<BookSummaryDto>{
        try {
            val (savedBook, siCreated) = BookService.createUpdate(isbn, book.toBookSummary())
            val responseCode = if(isCreated) HttpStatus.CREATED else HttpStatus.OK
            return ResponseEntity(saveBook.toBookSummaryDto(), responseCode)

        } catch(ex: IllegalStateException){
            return ResponseEntity(HttpStatus.INTENAL_SERVER_ERROR)
        } catch(ex: IllegalStateException){
            return ResponseEntity(HttpStatus.BAD_REQUEST)
        }
    }

   @GetMapping(path = ["/v1/books"])
   fun readManyBooks(@RequestPram("author")authorId: Long?): List<BookSummaryDto> {
        return BookService.list(authorId:).map { it.toBookSummaryDto()}
    }
}
```
②services/BooKService.ktを編集する

```services/BooKService.kt
package com.devtiro.bookstore.services

import com.devtiro.bookstore.domain.BookSummary
import com.devtiro.bookstore.domain.entity.BookEntity

interface BookService {

    fun createUpdate(isbn: String, bookSummary: BookSummary):Pair<BookEntity, Boolean>

    fun list(authorId: Long?): List<BookEntity>
}
```

③services/impl/BookServiceImpl.ktを作成する

```services/impl/BookServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.domain.BookSummary
import com.devtiro.bookstore.domain.entities.BookEntity
import com.devtiro.bookstore.repository.AuthorRepository
import com.devtiro.bookstore.repository.BookRepository
import com.devtiro.bookstore.services.BookService
import com.devtiro.bookstore.toBookEntity
import org.springframework.data.repository.findByIdOrNull


@Services
class BookServiceImpl(
    val bookRepository: bookRepository,
    val authorRepository: AuthorRepository,) :BookService {

    @Transactional
    override fun createUpdate(isbn: String, bookSummary: BookSummary): Pair<BookEntity, Boolean> {
        val normalisedBook = bookSummary.copy(isbn = isbn)
        val isExists = bookRepository.existsById(isbn)

        val author = authorRepository.findByIdOrNull(normalisedBook.author.id)
        checkNotNull(author)

        val saveBook = bookRepository.save(normalisedBook.toBookEntity(author))
        return Pair(saveBook, !isExists)
    }

    override fun list(): List<BookEntity> {
        return authorId?.let {
            bookRepository.findByAuthorEntityId(it)
        } ?: bookRepository.findAll()
    }
}
```
④repositories/BookRepositories.ktを作成する

```domain/BookRepository.kt
package com.devtiro.bookstore.repositories

import com.devtiro.bookstore.domain.BookEntity
import org.springframework.data.jpa.repositories.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface BookRepository : JpaRepository<Book, Long?> {

    fun findByAuthorEntityId(id: Long): List<BookEntity>
}
```
⑤domain/entities/AuthorEntity.ktを編集する

```domain/entities/AuthorEntity.kt
package com.devtiro.bookstore.domain

import jakarta.persistence.*

@Entity
@Table(name="author")
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "author_id_seq")
data class AuthorEntity (

    @Id
    @Column(name="id")
    val id: Long?,

    @Column(name="name")
    val name: String,

    @Column(name="age")
    val age: Int,

    @Column(name="description")
    val description: String,

    @Column(name="image")
    val image: String,

    @ManyToOne(mappedBy = "authorEntity", cascade=[CascadeType.REMOVE])
    val BookEntities: List<BookEntity> = emptyList(),
)
```
## Test Read Many Books Query

①test/kotlin/controllers/BooksControllerTest.ktを編集する

```test/kotlin/controllers/BooksControllerTest.kt
package com.devtiro.bookstore.controllers

import com.devtiro.bookstore.testAuthorEntityA
import com.devtiro.bookstore.testAuthorSummaryDtoA
import com.devtiro.bookstore.testBookEntityA
import com.devtiro.bookstore.testBookSummaryDtoA
import com.festerxml.jackson.databind.ObjectMapper
import com.ninjasquad.springmockk.MockBean
import org.hamcrest.CoreMatchers.equalTo
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.AutoConfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.put
import org.springframework.test.web.servlet.result.StatusResultMatchersDsl


@SpringBootTest
@AutoConfigureMockMvc
class BooksControllerTest @Autowired constructor(
    private val mockMvc: MockMvc,
    @MockkBean val authorService: AuthorService
    ) {
    val objectMapper = ObjectMapper()

    @Test
    fun `test that createFullUpdateBook return HTTP 201 when book is created`(){
        assertThatUserCreatedUpdated(true){isCreated()}
    }

    @Test
    fun `test that createFullUpdateBook return HTTP 200 when book is updated`(){
        assertThatUserCreatedUpdated(false){isOk()}
    }

    private fun assertThatUserCreatedUpdated(isCreated: Boolean, statusCodeAssertion: StatusResultMatchersDsl.() -> Unit) {
        val isbn = "978-089-230342-0777"
        val author = testAuthorEntityA(id=1)
        val savedBook = testBookEntityA(isbn, author)

        val authorSummaryDto = testBookSummaryDtoA(id=1)
        val bookSummaryDto = testBookSummaryDtoA(isbn, authorSummaryDto)

        every {
            bookService.createUpdate(isbn, any())
        } answers {
            Pair(savedBook, isCreated)
        }

        mockMvc.put("/v1/books/${isbn}"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookSummaryDto)
        }.andExpect {
            status { statusCodeAssertion()}
        }
    }


    @Test
    fun `test that createFullUpdateBook returns HTTP 500 when author in the database does not have an ID`(){
        val isbn = "978-089-230342-0777"
        val author = testAuthorEntityA(id=1)
        val savedBook = testBookEntityA(isbn, author)

        val authorSummaryDto = testBookSummaryDtoA(id=1)
        val bookSummaryDto = testBookSummaryDtoA(isbn, authorSummaryDto)

        every {
            bookService.createUpdate(isbn, any())
        } answers {
            Pair(savedBook, true)
        }

        mockMvc.put("/v1/books/${isbn}"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookSummaryDto)
        }.andExpect {
            status { isInternalServerError()}
        }
    }

    @Test
    fun `test that createFullUpdateBook returns HTTP 400 when author does not exist`(){
        val isbn = "978-089-230342-0777"
        
        val authorSummaryDto = testBookSummaryDtoA(id=1)
        val bookSummaryDto = testBookSummaryDtoA(isbn, authorSummaryDto)

        every {
            bookService.createUpdate(isbn, any())
        } throws IllegalStateException()

        mockMvc.put("/v1/books/${isbn}"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookSummaryDto)
        }.andExpect {
            status { isBadRequest()}
        }
    }

    @Test
    fun `test that readManyBooks returns a list of books`(){
        val isbn = "978-089-230342-0777"
        
        every {
            bookService.list()
        } answers {
            listOf(testAuthorEntityA(isbn = isbn, testAuthorEntityA(id=1)))
        }
        
        mockMvc.get("/v1/books"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk()}
            content { jsonPath ( "$[0].isbn", equalTo(isbn))}
            content { jsonPath ( "$[0].title", equalTo(isbn))}
            content { jsonPath ( "$[0].image", equalTo("book-image.jpeg"))}
            content { jsonPath ( "$[0].author.id", equalTo(1))}
            content { jsonPath ( "$[0].author.name", equalTo("John Doe"))}
            content { jsonPath ( "$[0].author.image", equalTo("author-image.jpeg"))}
        }
    }

    @Test
    fun `test that list returns no books when they do not match the author ID`(){
        every {
            bookService.list(authorId = any())
        } answers {
            emptyList()
        }

        mockMvc.get("/v1/books?author=999") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk()}
            content { jsonPath ("[]")}
        }
    }
    @Test
    fun `test that list returns books when matches the author ID`(){
        every {
            bookService.list(authorId = 1L)
        } answers {
            ListOf(
                testBookEntityA(
                    isbn=isbn,
                    testAuthorEntityA(1L)
                )
            )
        }

        mockMvc.get("/v1/books?author=1") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk()}
            content { jsonPath ( "$[0].isbn", equalTo(isbn))}
            content { jsonPath ( "$[0].title", equalTo(isbn))}
            content { jsonPath ( "$[0].image", equalTo("book-image.jpeg"))}
            content { jsonPath ( "$[0].author.id", equalTo(1))}
            content { jsonPath ( "$[0].author.name", equalTo("John Doe"))}
            content { jsonPath ( "$[0].author.image", equalTo("author-image.jpeg"))}
        }
    }

    @Test
    fun `test that readOneBook returns HTTP 404 when no book found`(){
        val isbn = "978-089-230342-0777"

        every {
            bookService.get(any())
        } answers {
            null
        }
        mockMvc.get("/v1/books/$isbn") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isNotFound()}
        }
    }

    @Test
    fun `test that readOneBook returns book  ane HTTP 200 when book found`(){
        val isbn = "978-089-230342-0777"

        every {
            bookService.get(isbn)
        } answers {
            testBookEntityA(isbn=isbn, testAuthorEntityA(id=1))
        }

        mockMvc.get("/v1/books/$isbn") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk()}
            content { jsonPath ( "$.isbn", equalTo(isbn))}
            content { jsonPath ( "$.title", equalTo(isbn))}
            content { jsonPath ( "$.image", equalTo("book-image.jpeg"))}
            content { jsonPath ( "$.author.id", equalTo(1))}
            content { jsonPath ( "$.author.name", equalTo("John Doe"))}
            content { jsonPath ( "$.author.image", equalTo("author-image.jpeg"))}
        }
    }
}
```
②services/BooKService.ktを編集する

```services/BooKService.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.domain.BookSummary
import com.devtiro.bookstore.domain.entities.BookEntity
import com.devtiro.bookstore.repository.AuthorRepository
import com.devtiro.bookstore.repository.BookRepository
import com.devtiro.bookstore.services.BookService
import com.devtiro.bookstore.toBookEntity
import org.springframework.data.repository.findByIdOrNull


@Services
class BookServiceImpl(
    val bookRepository: bookRepository,
    val authorRepository: AuthorRepository,) :BookService {

    @Transactional
    override fun createUpdate(isbn: String, bookSummary: BookSummary): Pair<BookEntity, Boolean> {
        val normalisedBook = bookSummary.copy(isbn = isbn)
        val isExists = bookRepository.existsById(isbn)

        val author = authorRepository.findByIdOrNull(normalisedBook.author.id)
        checkNotNull(author)

        val saveBook = bookRepository.save(normalisedBook.toBookEntity(author))
        return Pair(saveBook, !isExists)
    }

    override fun list(): List<BookEntity> {
        return authorId?.let {
            bookRepository.findByAuthorEntityId(it)
        } ?: bookRepository.findAll()
    }
}
```

③test/services/impl/BookServiceImplTest.ktを作成する

```test/services/impl/BookServiceImplTest.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.testAuthorEntityA
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.springframework.beans.factory.annoatation.Autowired
import org.springframework.boot.test.context.Transactional
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.database.repository.findByIdOrNull

@SpringBootTest
@Transactional
class BookServiceImplTest @Autowired constructor(

    private val underTest: AuthorServiceImpl,
    private val bookRepository: BookRepository,
    private val authorRepository: AuthorRepository
){

    @Test
    fun `test that createUpdate throws IllegalStateException when Author does not exist`(){
        val authorSummary = AuthorSummary(id=999L)
        val bookRepository = testBookSummaryA(BOOK_A_ISBN, authorSummary)
        assertThrows<IllegalStateException> {
        
        underTest.createUpdate(BOOK_A_ISBN, bookRepository)
        }
    }

    @Test
    fun `test that createUpdate throws successfully creates book in the database`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val authorSummary = AuthorSummary(id=savedAuthor!!)
        val bookRepository = testBookSummaryA(BOOK_A_ISBN, authorSummary)
        val (savedBook, isCreated) = underTest.createUpdate(BOOK_A_ISBN, bookRepository)
        assertThat(savedBook).isNotNull()

        val recalledBook = bookRepository.findByIdOrNull(BOOK_A_ISBN)
        assertThat(recalledBook).isNotNull()
        assertThat(recalledBook).isEqualTo(savedBook)
        assertThat(isCreated).isTrue()
    }

    @Test
    fun `test that createUpdate throws successfully update book in the database`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, saveAuthor))
        assertThat(savedBook).isNotNull()

        val authorSummary = AuthorSummary(id=savedAuthor!!)
        val bookRepository = testBookSummaryA(BOOK_A_ISBN, authorSummary)
        val (ipdateBook, isCreated) = underTest.createUpdate(BOOK_A_ISBN, bookRepository)
        assertThat(savedBook).isNotNull()

        val recalledBook = bookRepository.findByIdOrNull(BOOK_A_ISBN)
        assertThat(recalledBook).isNotNull()
        assertThat(isCreated).isFalse()
    }

    @Test
    fun `test that list returns an empty list when no book in the database`(){
        val result = underTest.list()
        assertThat(result).isEmpty()
    }

    @Test
    fun `test that list returns books list when no book in the database`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, savedAuthor))
        assertThat(savedBook).isNotNull()

        val result = underTest.list()
        assertThat(result).hasSize(1)
        assertThat(result.get[0]).isEqualTo(savedAuthor)
    }

    @Test
    fun `test that list returns no books when the author ID does not match`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, savedAuthor))
        assertThat(savedBook).isNotNull()

        val result = underTest.list(authorId = savedAuthor.id!! + 1)
        assertThat(result).hasSize(0)
    }

    @Test
    fun `test that list returns books when the author ID does match`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, savedAuthor))
        assertThat(savedBook).isNotNull()

        val result = underTest.list(authorId = savedAuthor.id)
        assertThat(result).hasSize(1)
        assertThat(result[0]).isEqualTo(savedBook)
    }

    @Test
    fun `test that list returns books when the author ID does match`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, savedAuthor))
        assertThat(savedBook).isNotNull()

        val result = underTest.list(authorId = savedAuthor.id)
        assertThat(result).hasSize(1)
        assertThat(result[0]).isEqualTo(savedBook)
    }

    @Test
    fun `test that get returns null when book not found in the database`(){
        val result = underTest.get(BOOK_A_ISBN)
        assertThat(result).isNull()
    }

    @Test
    fun `test that get returns book when the book is found in the database`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, savedAuthor))
        assertThat(savedBook).isNotNull()

        val result = underTest.get(savedAuthor.isbn)
        assertThat(result).isEqualTo(savedBook)
    }
}
```
## Book Partial Update

①kotlin/controllers/BooksController.ktを編集する

```kotlin/controllers/BooksController.kt
package com.devtiro.bookstore.controllers

import com.devtiro.bookstore.services.BookService
import com.devtiro.bookstore.domain.dto.toBookSummary
import com.devtiro.bookstore.domain.dto.toBookSummaryDto
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annoatation.*

@RestController
class BooksController(val BookService: BookService) {

    @PutMapping(path = ["/v1/books/{isbn}"])
    fun createFullUpdateBook(@PathVariable("isbn")isbn: String, @RequestBody book: BookSummaryDto): responseEntity<BookSummaryDto>{
        try {
            val (savedBook, siCreated) = BookService.createUpdate(isbn, book.toBookSummary())
            val responseCode = if(isCreated) HttpStatus.CREATED else HttpStatus.OK
            return ResponseEntity(saveBook.toBookSummaryDto(), responseCode)

        } catch(ex: IllegalStateException){
            return ResponseEntity(HttpStatus.INTENAL_SERVER_ERROR)
        } catch(ex: IllegalStateException){
            return ResponseEntity(HttpStatus.BAD_REQUEST)
        }
    }

   @GetMapping
   fun readManyBooks(@RequestPram("author")authorId: Long?): List<BookSummaryDto> {
        return bookService.list(authorId:).map { it.toBookSummaryDto()}
    }

    @GetMapping(path = ["/isbn"])
    fun readOneBooks(@PathVariable("isbn") isbn: String): ResponseEntity<BookSummaryDto> {
        return bookService.get(isbn:)?.let { ResponseEntity(it.toBookSummaryDto(), HttpStatus.OK)}
        ?: ResponseEntity(HttpStatus.NOT_FOUND)
    }

    @PatchMapping(path = ["/isbn"])
    fun partialUpdateBooks(
        @PathVariable("isbn") isbn: String, 
        @RequestBody BookUpdateRequestDto: BookUpdateRequestDto
    ): ResponseEntity<BookSummaryDto> {
        try {
            val updatedBook = bookService.partialUpdate(isbn, bookUpdateRequestDto.toBookUpdateRequest())
            return ResponseEntity(updatedBook.toAuthorSummaryDto(),HttpStatus.OK)
        } catch (ex: IllegalStateException)
            return ResponseEntity(HttpStatus.BAD_REQUEST)
    
    }
}
```
②domain/dto/BookUpdateRequestDto.ktを作成する

```domain/dto/BookUpdateRequestDto.kt
package com.devtiro.bookstore.domain.dto

data class BookUpdateRequestDto(
    var title: String?,
    var description: String?,
    var image: String?,
)
```
⓷services/BooKService.ktを編集する

```services/BooKService.kt
package com.devtiro.bookstore.services

import com.devtiro.bookstore.domain.BookSummary
import com.devtiro.bookstore.domain.entity.BookEntity

interface BookService {

    fun createUpdate(isbn: String, bookSummary: BookSummary): Pair<BookEntity, Boolean>

    fun list(authorId: Long?=null): List<BookEntity>

    fun get(isbn: String): BookEntity?

    fun partialUpdate(isbn: String, BookUpdateRequestDto: BookUpdateRequestDto): BookEntity
}
```

④services/impl/BookServiceImpl.ktを編集する

```services/impl/BookServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.domain.BookSummary
import com.devtiro.bookstore.domain.entities.BookEntity
import com.devtiro.bookstore.repository.AuthorRepository
import com.devtiro.bookstore.repository.BookRepository
import com.devtiro.bookstore.services.BookService
import com.devtiro.bookstore.toBookEntity
import org.springframework.data.repository.findByIdOrNull


@Services
class BookServiceImpl(
    val bookRepository: bookRepository,
    val authorRepository: AuthorRepository,) :BookService {

    @Transactional
    override fun createUpdate(isbn: String, bookSummary: BookSummary): Pair<BookEntity, Boolean> {
        val normalisedBook = bookSummary.copy(isbn = isbn)
        val isExists = bookRepository.existsById(isbn)

        val author = authorRepository.findByIdOrNull(normalisedBook.author.id)
        checkNotNull(author)

        val saveBook = bookRepository.save(normalisedBook.toBookEntity(author))
        return Pair(saveBook, !isExists)
    }

    override fun list(): List<BookEntity> {
        return authorId?.let {
            bookRepository.findByAuthorEntityId(it)
        } ?: bookRepository.findAll()
    }

    override fun get(isbn: String): BookEntity {
        return bookRepository.findByIdOrNull(isbn) 
    }

    override fun partialUpdate(isbn: String, bookUpdateRequest: BookUpdateRequest): BookEntity {
        val existingBook = bookRepository.findByIdOrNull(isbn)
        checkNotNull(existingBook)

        val updatedBook = existingBook.copy(
            title = bookUpdateRequest.title ?: existingBook.title,
            description = bookUpdateRequest.description ?:existingBook.description,
            image = bookUpdateRequest.image ?:existingBook.image,
        )

        return bookRepository.save(updatedBook)
    }
}
```
⑥kotlin/com/devtiro/bookstore/Extensions.ktを編集する

```kotlin/com/devtiro/bookstore/Extensions.kt
package com.devtiro.bookstore

import com.devtiro.bookstore.domain.dto.AuthorDto
import com.devtiro.bookstore.domain.dto.BookSummaryDto
import com.devtiro.bookstore.domain.entities.AuthorEntity
import com.devtiro.bookstore.domain.entities.BookEntity

fun AuthorEntity.toAuthorDto() = AuthorDto(
    id=this.id,
    name=this.name,
    age=this.age,
    description=this.description,
    image=this.image
)

fun AuthorEntity.toAuthorSummaryDto(): AuthorSummaryDto{
    val authorId = this.id ?:throw InvalidAuthorException()
    checkNotNull(authorId)
    return AuthorSummaryDto(
        id=this.id,
        name=this.name,
        image=this.image
    )
}

fun AuthorDto.toAuthorEntity() = AuthorEntity(
    id=this.id,
    name=this.name,
    age=this.age,
    description=this.description,
    image=this.image
)

fun AuthorUpdateRequestDto.toAuthorUpdateRequestDto() = AuthorUpdateRequest(
    id=this.id,
    name=this.name,
    age=this.age,
    description=this.description,
    image=this.image
)

fun BookSummary.toBookEntity(author: AuthorEntity) = BookEntity(
    isbn=this.isbn,
    title=this.title,
    description=this.description,
    image=this.image,
    authorEntity=this.authorEntity
)

fun AuthorSummaryDto.toAuthorSummary() = AuthorSummary(
    id=this.id,
    name=this.name,
    image=this.image
)

fun BookSummaryDto.toBookEntity(author: AuthorEntity) = BookSummary(
    isbn=this.isbn,
    title=this.title,
    description=this.description,
    image=this.image,
    author=this.author.toAuthorSummary()
)

fun BookEntity.toBookSummaryDto() = BookSummaryDto(
    isbn=this.isbn,
    title=this.title,
    description=this.description,
    image=this.image,
    author=this.authorEntityDto()
)

fun BookUpdateRequestDto.toBookUpdateRequest() = BookUpdateRequest(
    title=this.title,
    description=this.description,
    image=this.image
)
```

## Test Book Partial Update

①test/kotlin/controllers/BooksControllerTest.ktを編集する

```test/kotlin/controllers/BooksControllerTest.kt
package com.devtiro.bookstore.controllers

import com.devtiro.bookstore.testAuthorEntityA
import com.devtiro.bookstore.testAuthorSummaryDtoA
import com.devtiro.bookstore.testBookEntityA
import com.devtiro.bookstore.testBookSummaryDtoA
import com.festerxml.jackson.databind.ObjectMapper
import com.ninjasquad.springmockk.MockBean
import org.hamcrest.CoreMatchers.equalTo
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.AutoConfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.put
import org.springframework.test.web.servlet.result.StatusResultMatchersDsl


@SpringBootTest
@AutoConfigureMockMvc
class BooksControllerTest @Autowired constructor(
    private val mockMvc: MockMvc,
    @MockkBean val authorService: AuthorService
    ) {
    val objectMapper = ObjectMapper()

    @Test
    fun `test that createFullUpdateBook return HTTP 201 when book is created`(){
        assertThatUserCreatedUpdated(true){isCreated()}
    }

    @Test
    fun `test that createFullUpdateBook return HTTP 200 when book is updated`(){
        assertThatUserCreatedUpdated(false){isOk()}
    }

    private fun assertThatUserCreatedUpdated(isCreated: Boolean, statusCodeAssertion: StatusResultMatchersDsl.() -> Unit) {
        val isbn = "978-089-230342-0777"
        val author = testAuthorEntityA(id=1)
        val savedBook = testBookEntityA(isbn, author)

        val authorSummaryDto = testBookSummaryDtoA(id=1)
        val bookSummaryDto = testBookSummaryDtoA(isbn, authorSummaryDto)

        every {
            bookService.createUpdate(isbn, any())
        } answers {
            Pair(savedBook, isCreated)
        }

        mockMvc.put("/v1/books/${isbn}"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookSummaryDto)
        }.andExpect {
            status { statusCodeAssertion()}
        }
    }


    @Test
    fun `test that createFullUpdateBook returns HTTP 500 when author in the database does not have an ID`(){
        val isbn = "978-089-230342-0777"
        val author = testAuthorEntityA(id=1)
        val savedBook = testBookEntityA(isbn, author)

        val authorSummaryDto = testBookSummaryDtoA(id=1)
        val bookSummaryDto = testBookSummaryDtoA(isbn, authorSummaryDto)

        every {
            bookService.createUpdate(isbn, any())
        } answers {
            Pair(savedBook, true)
        }

        mockMvc.put("/v1/books/${isbn}"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookSummaryDto)
        }.andExpect {
            status { isInternalServerError()}
        }
    }

    @Test
    fun `test that createFullUpdateBook returns HTTP 400 when author does not exist`(){
        val isbn = "978-089-230342-0777"
        
        val authorSummaryDto = testBookSummaryDtoA(id=1)
        val bookSummaryDto = testBookSummaryDtoA(isbn, authorSummaryDto)

        every {
            bookService.createUpdate(isbn, any())
        } throws IllegalStateException()

        mockMvc.put("/v1/books/${isbn}"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookSummaryDto)
        }.andExpect {
            status { isBadRequest()}
        }
    }

    @Test
    fun `test that readManyBooks returns a list of books`(){
        val isbn = "978-089-230342-0777"
        
        every {
            bookService.list()
        } answers {
            listOf(testAuthorEntityA(isbn = isbn, testAuthorEntityA(id=1)))
        }
        
        mockMvc.get("/v1/books"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk()}
            content { jsonPath ( "$[0].isbn", equalTo(isbn))}
            content { jsonPath ( "$[0].title", equalTo(isbn))}
            content { jsonPath ( "$[0].image", equalTo("book-image.jpeg"))}
            content { jsonPath ( "$[0].author.id", equalTo(1))}
            content { jsonPath ( "$[0].author.name", equalTo("John Doe"))}
            content { jsonPath ( "$[0].author.image", equalTo("author-image.jpeg"))}
        }
    }

    @Test
    fun `test that list returns no books when they do not match the author ID`(){
        every {
            bookService.list(authorId = any())
        } answers {
            emptyList()
        }

        mockMvc.get("/v1/books?author=999") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk()}
            content { jsonPath ("[]")}
        }
    }
    @Test
    fun `test that list returns books when matches the author ID`(){
        every {
            bookService.list(authorId = 1L)
        } answers {
            ListOf(
                testBookEntityA(
                    isbn=isbn,
                    testAuthorEntityA(1L)
                )
            )
        }

        mockMvc.get("/v1/books?author=1") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk()}
            content { jsonPath ( "$[0].isbn", equalTo(isbn))}
            content { jsonPath ( "$[0].title", equalTo(isbn))}
            content { jsonPath ( "$[0].image", equalTo("book-image.jpeg"))}
            content { jsonPath ( "$[0].author.id", equalTo(1))}
            content { jsonPath ( "$[0].author.name", equalTo("John Doe"))}
            content { jsonPath ( "$[0].author.image", equalTo("author-image.jpeg"))}
        }
    }

    @Test
    fun `test that readOneBook returns HTTP 404 when no book found`(){
        val isbn = "978-089-230342-0777"

        every {
            bookService.get(any())
        } answers {
            null
        }
        mockMvc.get("/v1/books/$isbn") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isNotFound()}
        }
    }

    @Test
    fun `test that readOneBook returns book  ane HTTP 200 when book found`(){
        val isbn = "978-089-230342-0777"

        every {
            bookService.get(isbn)
        } answers {
            testBookEntityA(isbn=isbn, testAuthorEntityA(id=1))
        }

        mockMvc.get("/v1/books/$isbn") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk()}
            content { jsonPath ( "$.isbn", equalTo(isbn))}
            content { jsonPath ( "$.title", equalTo(isbn))}
            content { jsonPath ( "$.image", equalTo("book-image.jpeg"))}
            content { jsonPath ( "$.author.id", equalTo(1))}
            content { jsonPath ( "$.author.name", equalTo("John Doe"))}
            content { jsonPath ( "$.author.image", equalTo("author-image.jpeg"))}
        }
    }

    @Test
    fun `test that bookPartialUpdate returns a HTTP 400 on IllegalStateException`(){
        
        val title = "A new title",
        val bookUpdateRequest = BookUpdateRequest(
            title = title,
        )

        val bookUpdateRequestDto = BookUpdateRequestDto(
            title = title,
        )

        every {
            bookService.partialUpdate(BOOK_A_ISBN, bookUpdateRequest)
        } throws IllegalStateException()

        mockMvc.patch("/v1/books/$BOOK_A_ISBN") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookUpdateRequestDto)
        }.andExpect {
            status { isBadRequest()}
        }
    }
    @Test
    fun `test that bookPartialUpdate returns a HTTP 200 and book on a successfu update`(){
        val title = "A new title",
        val bookUpdateRequest = BookUpdateRequest(
            title = title,
        )

        val bookUpdateRequestDto = BookUpdateRequestDto(
            title = title,
        )

        val BookEntity = testBookEntityA(BOOK_A_ISBN, testBookEntityA(id=1))

        every {
            bookService.partialUpdate(BOOK_A_ISBN, bookUpdateRequest)
        } answers {
            BookEntity
        }

        mockMvc.patch("/v1/books/$BOOK_A_ISBN") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookUpdateRequestDto)
        }.andExpect {
            status { isBadRequest()}
            content { jsonPath ( "$.isbn", equalTo(isbn))}
            content { jsonPath ( "$.title", equalTo(isbn))}
            content { jsonPath ( "$.image", equalTo("book-image.jpeg"))}
            content { jsonPath ( "$.author.id", equalTo(1))}
            content { jsonPath ( "$.author.name", equalTo("John Doe"))}
            content { jsonPath ( "$.author.image", equalTo("author-image.jpeg"))}
        }

    }
}
```

## Test Book Partial Update

①test/kotlin/controllers/BooksControllerTest.ktを編集する

```test/kotlin/controllers/BooksControllerTest.kt
package com.devtiro.bookstore.controllers

import com.devtiro.bookstore.testAuthorEntityA
import com.devtiro.bookstore.testAuthorSummaryDtoA
import com.devtiro.bookstore.testBookEntityA
import com.devtiro.bookstore.testBookSummaryDtoA
import com.festerxml.jackson.databind.ObjectMapper
import com.ninjasquad.springmockk.MockBean
import org.hamcrest.CoreMatchers.equalTo
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.AutoConfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.put
import org.springframework.test.web.servlet.result.StatusResultMatchersDsl


@SpringBootTest
@AutoConfigureMockMvc
class BooksControllerTest @Autowired constructor(
    private val mockMvc: MockMvc,
    @MockkBean val authorService: AuthorService
    ) {
    val objectMapper = ObjectMapper()

    @Test
    fun `test that createFullUpdateBook return HTTP 201 when book is created`(){
        assertThatUserCreatedUpdated(true){isCreated()}
    }

    @Test
    fun `test that createFullUpdateBook return HTTP 200 when book is updated`(){
        assertThatUserCreatedUpdated(false){isOk()}
    }

    private fun assertThatUserCreatedUpdated(isCreated: Boolean, statusCodeAssertion: StatusResultMatchersDsl.() -> Unit) {
        val isbn = "978-089-230342-0777"
        val author = testAuthorEntityA(id=1)
        val savedBook = testBookEntityA(isbn, author)

        val authorSummaryDto = testBookSummaryDtoA(id=1)
        val bookSummaryDto = testBookSummaryDtoA(isbn, authorSummaryDto)

        every {
            bookService.createUpdate(isbn, any())
        } answers {
            Pair(savedBook, isCreated)
        }

        mockMvc.put("/v1/books/${isbn}"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookSummaryDto)
        }.andExpect {
            status { statusCodeAssertion()}
        }
    }


    @Test
    fun `test that createFullUpdateBook returns HTTP 500 when author in the database does not have an ID`(){
        val isbn = "978-089-230342-0777"
        val author = testAuthorEntityA(id=1)
        val savedBook = testBookEntityA(isbn, author)

        val authorSummaryDto = testBookSummaryDtoA(id=1)
        val bookSummaryDto = testBookSummaryDtoA(isbn, authorSummaryDto)

        every {
            bookService.createUpdate(isbn, any())
        } answers {
            Pair(savedBook, true)
        }

        mockMvc.put("/v1/books/${isbn}"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookSummaryDto)
        }.andExpect {
            status { isInternalServerError()}
        }
    }

    @Test
    fun `test that createFullUpdateBook returns HTTP 400 when author does not exist`(){
        val isbn = "978-089-230342-0777"
        
        val authorSummaryDto = testBookSummaryDtoA(id=1)
        val bookSummaryDto = testBookSummaryDtoA(isbn, authorSummaryDto)

        every {
            bookService.createUpdate(isbn, any())
        } throws IllegalStateException()

        mockMvc.put("/v1/books/${isbn}"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookSummaryDto)
        }.andExpect {
            status { isBadRequest()}
        }
    }

    @Test
    fun `test that readManyBooks returns a list of books`(){
        val isbn = "978-089-230342-0777"
        
        every {
            bookService.list()
        } answers {
            listOf(testAuthorEntityA(isbn = isbn, testAuthorEntityA(id=1)))
        }
        
        mockMvc.get("/v1/books"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk()}
            content { jsonPath ( "$[0].isbn", equalTo(isbn))}
            content { jsonPath ( "$[0].title", equalTo(isbn))}
            content { jsonPath ( "$[0].image", equalTo("book-image.jpeg"))}
            content { jsonPath ( "$[0].author.id", equalTo(1))}
            content { jsonPath ( "$[0].author.name", equalTo("John Doe"))}
            content { jsonPath ( "$[0].author.image", equalTo("author-image.jpeg"))}
        }
    }

    @Test
    fun `test that list returns no books when they do not match the author ID`(){
        every {
            bookService.list(authorId = any())
        } answers {
            emptyList()
        }

        mockMvc.get("/v1/books?author=999") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk()}
            content { jsonPath ("[]")}
        }
    }
    @Test
    fun `test that list returns books when matches the author ID`(){
        every {
            bookService.list(authorId = 1L)
        } answers {
            ListOf(
                testBookEntityA(
                    isbn=isbn,
                    testAuthorEntityA(1L)
                )
            )
        }

        mockMvc.get("/v1/books?author=1") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk()}
            content { jsonPath ( "$[0].isbn", equalTo(isbn))}
            content { jsonPath ( "$[0].title", equalTo(isbn))}
            content { jsonPath ( "$[0].image", equalTo("book-image.jpeg"))}
            content { jsonPath ( "$[0].author.id", equalTo(1))}
            content { jsonPath ( "$[0].author.name", equalTo("John Doe"))}
            content { jsonPath ( "$[0].author.image", equalTo("author-image.jpeg"))}
        }
    }

    @Test
    fun `test that readOneBook returns HTTP 404 when no book found`(){
        val isbn = "978-089-230342-0777"

        every {
            bookService.get(any())
        } answers {
            null
        }
        mockMvc.get("/v1/books/$isbn") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isNotFound()}
        }
    }

    @Test
    fun `test that readOneBook returns book  ane HTTP 200 when book found`(){
        val isbn = "978-089-230342-0777"

        every {
            bookService.get(isbn)
        } answers {
            testBookEntityA(isbn=isbn, testAuthorEntityA(id=1))
        }

        mockMvc.get("/v1/books/$isbn") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk()}
            content { jsonPath ( "$.isbn", equalTo(isbn))}
            content { jsonPath ( "$.title", equalTo(isbn))}
            content { jsonPath ( "$.image", equalTo("book-image.jpeg"))}
            content { jsonPath ( "$.author.id", equalTo(1))}
            content { jsonPath ( "$.author.name", equalTo("John Doe"))}
            content { jsonPath ( "$.author.image", equalTo("author-image.jpeg"))}
        }
    }

    @Test
    fun `test that bookPartialUpdate returns a HTTP 400 on IllegalStateException`(){
        
        val title = "A new title",
        val bookUpdateRequest = BookUpdateRequest(
            title = title,
        )

        val bookUpdateRequestDto = BookUpdateRequestDto(
            title = title,
        )

        every {
            bookService.partialUpdate(BOOK_A_ISBN, bookUpdateRequest)
        } throws IllegalStateException()

        mockMvc.patch("/v1/books/$BOOK_A_ISBN") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookUpdateRequestDto)
        }.andExpect {
            status { isBadRequest()}
        }
    }
    @Test
    fun `test that bookPartialUpdate returns a HTTP 200 and book on a successful update`(){
        val title = "A new title",
        val bookUpdateRequest = BookUpdateRequest(
            title = title,
        )

        val bookUpdateRequestDto = BookUpdateRequestDto(
            title = title,
        )

        val BookEntity = testBookEntityA(BOOK_A_ISBN, testBookEntityA(id=1))

        every {
            bookService.partialUpdate(BOOK_A_ISBN, bookUpdateRequest)
        } answers {
            BookEntity
        }

        mockMvc.patch("/v1/books/$BOOK_A_ISBN") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookUpdateRequestDto)
        }.andExpect {
            status { isBadRequest()}
            content { jsonPath ( "$.isbn", equalTo(isbn))}
            content { jsonPath ( "$.title", equalTo(isbn))}
            content { jsonPath ( "$.image", equalTo("book-image.jpeg"))}
            content { jsonPath ( "$.author.id", equalTo(1))}
            content { jsonPath ( "$.author.name", equalTo("John Doe"))}
            content { jsonPath ( "$.author.image", equalTo("author-image.jpeg"))}
        }
    }
    @Test
    fun `test that bookPartialUpdate returns a HTTP 400 and book on IllegalStateException`(){
        title = "A new title",
        val bookUpdateRequest = BookUpdateRequest(
            title = title,
        )

        val bookUpdateRequestDto = BookUpdateRequestDto(
            title = title,
        )

        every {
            bookService.partialUpdate(BOOK_A_ISBN, bookUpdateRequest)
        } throws IllegalStateException()

        mockMvc.patch("/v1/books/$BOOK_A_ISBN") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookUpdateRequestDto)
        }.andExpect {
            status { isBadRequest()}
        }
    }
    @Test
    fun `test that bookPartialUpdate returns a HTTP 200 and book on a successful update`(){
        title = "A new title",
        val bookUpdateRequest = BookUpdateRequest(
            title = title,
        )

        val bookUpdateRequestDto = BookUpdateRequestDto(
            title = title,
        )

        val BookEntity = testBookEntityA(BOOK_A_ISBN, testAuthorEntityA(id=1))

        every {
            bookService.partialUpdate(BOOK_A_ISBN, bookUpdateRequest)
        } answers {
            BookEntity
        }

        mockMvc.patch("/v1/books/$BOOK_A_ISBN") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookUpdateRequestDto)
        }.andExpect {
            status { isBadRequest()}
            content { jsonPath ( "$.isbn", equalTo(BOOK_A_ISBN))}
            content { jsonPath ( "$.title", equalTo("Test Book A"))}
            content { jsonPath ( "$.image", equalTo("book-image.jpeg"))}
            content { jsonPath ( "$.author.id", equalTo(1))}
            content { jsonPath ( "$.author.name", equalTo("John Doe"))}
            content { jsonPath ( "$.author.image", equalTo("author-image.jpeg"))}
        }
    }
}
```
②services/impl/BookServiceImpl.ktを編集する

```services/impl/BookServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.testAuthorEntityA
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.springframework.beans.factory.annoatation.Autowired
import org.springframework.boot.test.context.Transactional
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.database.repository.findByIdOrNull

@SpringBootTest
@Transactional
class BookServiceImplTest @Autowired constructor(

    private val underTest: AuthorServiceImpl,
    private val bookRepository: BookRepository,
    private val authorRepository: AuthorRepository
){

    @Test
    fun `test that createUpdate throws IllegalStateException when Author does not exist`(){
        val authorSummary = AuthorSummary(id=999L)
        val bookRepository = testBookSummaryA(BOOK_A_ISBN, authorSummary)
        assertThrows<IllegalStateException> {
        
        underTest.createUpdate(BOOK_A_ISBN, bookRepository)
        }
    }

    @Test
    fun `test that createUpdate throws successfully creates book in the database`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val authorSummary = AuthorSummary(id=savedAuthor!!)
        val bookRepository = testBookSummaryA(BOOK_A_ISBN, authorSummary)
        val (savedBook, isCreated) = underTest.createUpdate(BOOK_A_ISBN, bookRepository)
        assertThat(savedBook).isNotNull()

        val recalledBook = bookRepository.findByIdOrNull(BOOK_A_ISBN)
        assertThat(recalledBook).isNotNull()
        assertThat(recalledBook).isEqualTo(savedBook)
        assertThat(isCreated).isTrue()
    }

    @Test
    fun `test that createUpdate throws successfully update book in the database`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, saveAuthor))
        assertThat(savedBook).isNotNull()

        val authorSummary = AuthorSummary(id=savedAuthor!!)
        val bookRepository = testBookSummaryA(BOOK_A_ISBN, authorSummary)
        val (ipdateBook, isCreated) = underTest.createUpdate(BOOK_A_ISBN, bookRepository)
        assertThat(savedBook).isNotNull()

        val recalledBook = bookRepository.findByIdOrNull(BOOK_A_ISBN)
        assertThat(recalledBook).isNotNull()
        assertThat(isCreated).isFalse()
    }

    @Test
    fun `test that list returns an empty list when no book in the database`(){
        val result = underTest.list()
        assertThat(result).isEmpty()
    }

    @Test
    fun `test that list returns books list when no book in the database`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, savedAuthor))
        assertThat(savedBook).isNotNull()

        val result = underTest.list()
        assertThat(result).hasSize(1)
        assertThat(result.get[0]).isEqualTo(savedAuthor)
    }

    @Test
    fun `test that list returns no books when the author ID does not match`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, savedAuthor))
        assertThat(savedBook).isNotNull()

        val result = underTest.list(authorId = savedAuthor.id!! + 1)
        assertThat(result).hasSize(0)
    }

    @Test
    fun `test that list returns books when the author ID does match`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, savedAuthor))
        assertThat(savedBook).isNotNull()

        val result = underTest.list(authorId = savedAuthor.id)
        assertThat(result).hasSize(1)
        assertThat(result[0]).isEqualTo(savedBook)
    }

    @Test
    fun `test that list returns books when the author ID does match`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, savedAuthor))
        assertThat(savedBook).isNotNull()

        val result = underTest.list(authorId = savedAuthor.id)
        assertThat(result).hasSize(1)
        assertThat(result[0]).isEqualTo(savedBook)
    }

    @Test
    fun `test that get returns null when book not found in the database`(){
        val result = underTest.get(BOOK_A_ISBN)
        assertThat(result).isNull()
    }

    @Test
    fun `test that get returns book when the book is found in the database`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, savedAuthor))
        assertThat(savedBook).isNotNull()

        val result = underTest.get(savedAuthor.isbn)
        assertThat(result).isEqualTo(savedBook)
    }
    @Test
    fun `test that package throws IllegalStateException when the book does not exist in the database`(){
        assertThrows<IllegalStateException> {
        val bookUpdateRequest = BookUpdateRequest(
        title = "A new title"
        )
        underTest.partialUpdate(BOOK_A_ISBN, bookUpdateRequest)
        }
    }

    @Test
    fun `test that partialUpdate updates the title of an existing book`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, savedAuthor))
        assertThat(savedBook).isNotNull()

        val newTitle = "A new title"
        val bookUpdateRequest = BookUpdateRequest(
            title = newTitle
        )

        underTest.partialUpdate(BOOK_A_ISBN, bookUpdateRequest)
        assertThat(result.title).isEqualTo(newTitle)
    }

    @Test
    fun `test that partialUpdate updates the description of an existing book`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, savedAuthor))
        assertThat(savedBook).isNotNull()

        val newDescription = "A new description"
        val bookUpdateRequest = BookUpdateRequest(
            description = newDescription
        )
        underTest.partialUpdate(BOOK_A_ISBN, bookUpdateRequest)
        assertThat(result.description).isEqualTo(newDescription)
    }

    @Test
    fun `test that partialUpdate updates the image of an existing book`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, savedAuthor))
        assertThat(savedBook).isNotNull()

        val newImage = "A new image"
        val bookUpdateRequest = BookUpdateRequest(
            image = newImage
        )
        underTest.partialUpdate(BOOK_A_ISBN, bookUpdateRequest)
        assertThat(result.image).isEqualTo(newImage)
    }
}
```
## Book Delete
①kotlin/controllers/BooksController.ktを編集する

```kotlin/controllers/BooksController.kt
package com.devtiro.bookstore.controllers

import com.devtiro.bookstore.services.BookService
import com.devtiro.bookstore.domain.dto.toBookSummary
import com.devtiro.bookstore.domain.dto.toBookSummaryDto
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annoatation.*

@RestController
class BooksController(val BookService: BookService) {

    @PutMapping(path = ["/v1/books/{isbn}"])
    fun createFullUpdateBook(@PathVariable("isbn")isbn: String, @RequestBody book: BookSummaryDto): responseEntity<BookSummaryDto> {
        try {
            val (savedBook, siCreated) = BookService.createUpdate(isbn, book.toBookSummary())
            val responseCode = if(isCreated) HttpStatus.CREATED else HttpStatus.OK
            return ResponseEntity(saveBook.toBookSummaryDto(), responseCode)

        } catch(ex: IllegalStateException){
            return ResponseEntity(HttpStatus.INTENAL_SERVER_ERROR)
        } catch(ex: IllegalStateException){
            return ResponseEntity(HttpStatus.BAD_REQUEST)
        }
    }

   @GetMapping
   fun readManyBooks(@RequestPram("author")authorId: Long?): List<BookSummaryDto> {
        return bookService.list(authorId:).map { it.toBookSummaryDto()}
    }

    @GetMapping(path = ["/isbn"])
    fun readOneBooks(@PathVariable("isbn") isbn: String): ResponseEntity<BookSummaryDto> {
        return bookService.get(isbn:)?.let { ResponseEntity(it.toBookSummaryDto(), HttpStatus.OK)}
        ?: ResponseEntity(HttpStatus.NOT_FOUND)
    }

    @PatchMapping(path = ["/isbn"])
    fun partialUpdateBooks(
        @PathVariable("isbn") isbn: String,
        @RequestBody BookUpdateRequestDto: BookUpdateRequestDto
    ): ResponseEntity<BookSummaryDto> {
        try {
            val updatedBook = bookService.partialUpdate(isbn, bookUpdateRequestDto.toBookUpdateRequest())
            return ResponseEntity(updatedBook.toAuthorSummaryDto(),HttpStatus.OK)
        } catch (ex: IllegalStateException) {
            return ResponseEntity(HttpStatus.BAD_REQUEST)
        }
    }

    @DeleteMapping(path = ["/isbn"])
    fun deleteBooks(@PathVariable("isbn") isbn: String): ResponseEntity<Unit> {
        bookService.delete(isbn)
        return ResponseEntity(HttpStatus.NO_CONTENT)
    }
}
```
②services/BooKService.ktを編集する

```services/BooKService.kt
package com.devtiro.bookstore.services

import com.devtiro.bookstore.domain.BookSummary
import com.devtiro.bookstore.domain.BookUpdateRequest
import com.devtiro.bookstore.domain.entities.BookEntity

interface BookService {

    fun createUpdate(isbn: String, bookSummary: BookSummary): Pair<BookEntity, Boolean>

    fun list(authorId: Long?=null): List<BookEntity>

    fun get(isbn: String): BookEntity?

    fun partialUpdate(isbn: String, BookUpdateRequestDto: BookUpdateRequestDto): BookEntity

    fun delete(isbn: String): Unit
}
```
⓷services/impl/BookServiceImpl.ktを編集する

```services/impl/BookServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.domain.BookSummary
import com.devtiro.bookstore.domain.BookUpdateRequest
import com.devtiro.bookstore.domain.entities.BookEntity
import com.devtiro.bookstore.repository.AuthorRepository
import com.devtiro.bookstore.repository.BookRepository
import com.devtiro.bookstore.services.BookService
import com.devtiro.bookstore.toBookEntity
import jakarta.transaction.Transactional
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Services

@Services
class BookServiceImpl(
    val bookRepository: bookRepository,
    val authorRepository: AuthorRepository,) :BookService {

    @Transactional
    override fun createUpdate(isbn: String, bookSummary: BookSummary): Pair<BookEntity, Boolean> {
        val normalisedBook = bookSummary.copy(isbn = isbn)
        val isExists = bookRepository.existsById(isbn)

        val author = authorRepository.findByIdOrNull(normalisedBook.author.id)
        checkNotNull(author)

        val saveBook = bookRepository.save(normalisedBook.toBookEntity(author))
        return Pair(saveBook, !isExists)
    }

    override fun list(): List<BookEntity> {
        return authorId?.let {
            bookRepository.findByAuthorEntityId(it)
        } ?: bookRepository.findAll()
    }

    override fun get(isbn: String): BookEntity {
        return bookRepository.findByIdOrNull(isbn) 
    }

    override fun partialUpdate(isbn: String, bookUpdateRequest: BookUpdateRequest): BookEntity {
        val existingBook = bookRepository.findByIdOrNull(isbn)
        checkNotNull(existingBook)

        val updatedBook = existingBook.copy(
            title = bookUpdateRequest.title ?: existingBook.title,
            description = bookUpdateRequest.description ?:existingBook.description,
            image = bookUpdateRequest.image ?:existingBook.image,
        )

        return bookRepository.save(updatedBook)
    }

    override fun delete(isbn: String){
        bookRepository.deleteById(isbn)
    }
}
```
## Test Book Delete

①test/kotlin/controllers/BooksControllerTest.ktを編集する

```test/kotlin/controllers/BooksControllerTest.kt
package com.devtiro.bookstore.controllers

import com.devtiro.bookstore.testAuthorEntityA
import com.devtiro.bookstore.testAuthorSummaryDtoA
import com.devtiro.bookstore.testBookEntityA
import com.devtiro.bookstore.testBookSummaryDtoA
import com.festerxml.jackson.databind.ObjectMapper
import com.ninjasquad.springmockk.MockBean
import org.hamcrest.CoreMatchers.equalTo
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.AutoConfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.get
import org.springframework.test.web.servlet.put
import org.springframework.test.web.servlet.result.StatusResultMatchersDsl


@SpringBootTest
@AutoConfigureMockMvc
class BooksControllerTest @Autowired constructor(
    private val mockMvc: MockMvc,
    @MockkBean val authorService: AuthorService
    ) {
    val objectMapper = ObjectMapper()

    @Test
    fun `test that createFullUpdateBook return HTTP 201 when book is created`(){
        assertThatUserCreatedUpdated(true){isCreated()}
    }

    @Test
    fun `test that createFullUpdateBook return HTTP 200 when book is updated`(){
        assertThatUserCreatedUpdated(false){isOk()}
    }

    private fun assertThatUserCreatedUpdated(isCreated: Boolean, statusCodeAssertion: StatusResultMatchersDsl.() -> Unit) {
        val isbn = "978-089-230342-0777"
        val author = testAuthorEntityA(id=1)
        val savedBook = testBookEntityA(isbn, author)

        val authorSummaryDto = testBookSummaryDtoA(id=1)
        val bookSummaryDto = testBookSummaryDtoA(isbn, authorSummaryDto)

        every {
            bookService.createUpdate(isbn, any())
        } answers {
            Pair(savedBook, isCreated)
        }

        mockMvc.put("/v1/books/${isbn}"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookSummaryDto)
        }.andExpect {
            status { statusCodeAssertion()}
        }
    }


    @Test
    fun `test that createFullUpdateBook returns HTTP 500 when author in the database does not have an ID`(){
        val isbn = "978-089-230342-0777"
        val author = testAuthorEntityA(id=1)
        val savedBook = testBookEntityA(isbn, author)

        val authorSummaryDto = testBookSummaryDtoA(id=1)
        val bookSummaryDto = testBookSummaryDtoA(isbn, authorSummaryDto)

        every {
            bookService.createUpdate(isbn, any())
        } answers {
            Pair(savedBook, true)
        }

        mockMvc.put("/v1/books/${isbn}"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookSummaryDto)
        }.andExpect {
            status { isInternalServerError()}
        }
    }

    @Test
    fun `test that createFullUpdateBook returns HTTP 400 when author does not exist`(){
        val isbn = "978-089-230342-0777"
        
        val authorSummaryDto = testBookSummaryDtoA(id=1)
        val bookSummaryDto = testBookSummaryDtoA(isbn, authorSummaryDto)

        every {
            bookService.createUpdate(isbn, any())
        } throws IllegalStateException()

        mockMvc.put("/v1/books/${isbn}"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookSummaryDto)
        }.andExpect {
            status { isBadRequest()}
        }
    }

    @Test
    fun `test that readManyBooks returns a list of books`(){
        val isbn = "978-089-230342-0777"
        
        every {
            bookService.list()
        } answers {
            listOf(testAuthorEntityA(isbn = isbn, testAuthorEntityA(id=1)))
        }
        
        mockMvc.get("/v1/books"){
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk()}
            content { jsonPath ( "$[0].isbn", equalTo(isbn))}
            content { jsonPath ( "$[0].title", equalTo(isbn))}
            content { jsonPath ( "$[0].image", equalTo("book-image.jpeg"))}
            content { jsonPath ( "$[0].author.id", equalTo(1))}
            content { jsonPath ( "$[0].author.name", equalTo("John Doe"))}
            content { jsonPath ( "$[0].author.image", equalTo("author-image.jpeg"))}
        }
    }

    @Test
    fun `test that list returns no books when they do not match the author ID`(){
        every {
            bookService.list(authorId = any())
        } answers {
            emptyList()
        }

        mockMvc.get("/v1/books?author=999") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk()}
            content { jsonPath ("[]")}
        }
    }
    @Test
    fun `test that list returns books when matches the author ID`(){
        every {
            bookService.list(authorId = 1L)
        } answers {
            ListOf(
                testBookEntityA(
                    isbn=isbn,
                    testAuthorEntityA(1L)
                )
            )
        }

        mockMvc.get("/v1/books?author=1") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk()}
            content { jsonPath ( "$[0].isbn", equalTo(isbn))}
            content { jsonPath ( "$[0].title", equalTo(isbn))}
            content { jsonPath ( "$[0].image", equalTo("book-image.jpeg"))}
            content { jsonPath ( "$[0].author.id", equalTo(1))}
            content { jsonPath ( "$[0].author.name", equalTo("John Doe"))}
            content { jsonPath ( "$[0].author.image", equalTo("author-image.jpeg"))}
        }
    }

    @Test
    fun `test that readOneBook returns HTTP 404 when no book found`(){
        val isbn = "978-089-230342-0777"

        every {
            bookService.get(any())
        } answers {
            null
        }
        mockMvc.get("/v1/books/$isbn") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isNotFound()}
        }
    }

    @Test
    fun `test that readOneBook returns book  ane HTTP 200 when book found`(){
        val isbn = "978-089-230342-0777"

        every {
            bookService.get(isbn)
        } answers {
            testBookEntityA(isbn=isbn, testAuthorEntityA(id=1))
        }

        mockMvc.get("/v1/books/$isbn") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOk()}
            content { jsonPath ( "$.isbn", equalTo(isbn))}
            content { jsonPath ( "$.title", equalTo(isbn))}
            content { jsonPath ( "$.image", equalTo("book-image.jpeg"))}
            content { jsonPath ( "$.author.id", equalTo(1))}
            content { jsonPath ( "$.author.name", equalTo("John Doe"))}
            content { jsonPath ( "$.author.image", equalTo("author-image.jpeg"))}
        }
    }

    @Test
    fun `test that bookPartialUpdate returns a HTTP 400 on IllegalStateException`(){
        
        val title = "A new title",
        val bookUpdateRequest = BookUpdateRequest(
            title = title,
        )

        val bookUpdateRequestDto = BookUpdateRequestDto(
            title = title,
        )

        every {
            bookService.partialUpdate(BOOK_A_ISBN, bookUpdateRequest)
        } throws IllegalStateException()

        mockMvc.patch("/v1/books/$BOOK_A_ISBN") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookUpdateRequestDto)
        }.andExpect {
            status { isBadRequest()}
        }
    }
    @Test
    fun `test that bookPartialUpdate returns a HTTP 200 and book on a successful update`(){
        val title = "A new title",
        val bookUpdateRequest = BookUpdateRequest(
            title = title,
        )

        val bookUpdateRequestDto = BookUpdateRequestDto(
            title = title,
        )

        val BookEntity = testBookEntityA(BOOK_A_ISBN, testBookEntityA(id=1))

        every {
            bookService.partialUpdate(BOOK_A_ISBN, bookUpdateRequest)
        } answers {
            BookEntity
        }

        mockMvc.patch("/v1/books/$BOOK_A_ISBN") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookUpdateRequestDto)
        }.andExpect {
            status { isBadRequest()}
            content { jsonPath ( "$.isbn", equalTo(isbn))}
            content { jsonPath ( "$.title", equalTo(isbn))}
            content { jsonPath ( "$.image", equalTo("book-image.jpeg"))}
            content { jsonPath ( "$.author.id", equalTo(1))}
            content { jsonPath ( "$.author.name", equalTo("John Doe"))}
            content { jsonPath ( "$.author.image", equalTo("author-image.jpeg"))}
        }
    }
    @Test
    fun `test that bookPartialUpdate returns a HTTP 400 and book on IllegalStateException`(){
        title = "A new title",
        val bookUpdateRequest = BookUpdateRequest(
            title = title,
        )

        val bookUpdateRequestDto = BookUpdateRequestDto(
            title = title,
        )

        every {
            bookService.partialUpdate(BOOK_A_ISBN, bookUpdateRequest)
        } throws IllegalStateException()

        mockMvc.patch("/v1/books/$BOOK_A_ISBN") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookUpdateRequestDto)
        }.andExpect {
            status { isBadRequest()}
        }
    }
    @Test
    fun `test that bookPartialUpdate returns a HTTP 200 and book on a successful update`(){
        title = "A new title",
        val bookUpdateRequest = BookUpdateRequest(
            title = title,
        )

        val bookUpdateRequestDto = BookUpdateRequestDto(
            title = title,
        )

        val BookEntity = testBookEntityA(BOOK_A_ISBN, testAuthorEntityA(id=1))

        every {
            bookService.partialUpdate(BOOK_A_ISBN, bookUpdateRequest)
        } answers {
            BookEntity
        }

        mockMvc.patch("/v1/books/$BOOK_A_ISBN") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
            content = objectMapper.writeValueAsString(bookUpdateRequestDto)
        }.andExpect {
            status { isBadRequest()}
            content { jsonPath ( "$.isbn", equalTo(BOOK_A_ISBN))}
            content { jsonPath ( "$.title", equalTo("Test Book A"))}
            content { jsonPath ( "$.image", equalTo("book-image.jpeg"))}
            content { jsonPath ( "$.author.id", equalTo(1))}
            content { jsonPath ( "$.author.name", equalTo("John Doe"))}
            content { jsonPath ( "$.author.image", equalTo("author-image.jpeg"))}
        }
    }

    @Test
    fun `test that deleteBook deletes a book successfully`(){
        every {
            bookService.delete(BOOK_A_ISBN)
        } answers {
        }

        mockMvc.delete("/v1/books/$BOOK_A_ISBN") {
            contentType = MediaType.APPLICATION_JSON
            accept = MediaType.APPLICATION_JSON
        }.andExpect {
            status { isOK()}
    }
}

```

②services/impl/BookServiceImpl.ktを編集する

```services/impl/BookServiceImpl.kt
package com.devtiro.bookstore.services.impl

import com.devtiro.bookstore.testAuthorEntityA
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.springframework.beans.factory.annoatation.Autowired
import org.springframework.boot.test.context.Transactional
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.database.repository.findByIdOrNull

@SpringBootTest
@Transactional
class BookServiceImplTest @Autowired constructor(

    private val underTest: AuthorServiceImpl,
    private val bookRepository: BookRepository,
    private val authorRepository: AuthorRepository
){

    @Test
    fun `test that createUpdate throws IllegalStateException when Author does not exist`(){
        val authorSummary = AuthorSummary(id=999L)
        val bookRepository = testBookSummaryA(BOOK_A_ISBN, authorSummary)
        assertThrows<IllegalStateException> {
        
        underTest.createUpdate(BOOK_A_ISBN, bookRepository)
        }
    }

    @Test
    fun `test that createUpdate throws successfully creates book in the database`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val authorSummary = AuthorSummary(id=savedAuthor!!)
        val bookRepository = testBookSummaryA(BOOK_A_ISBN, authorSummary)
        val (savedBook, isCreated) = underTest.createUpdate(BOOK_A_ISBN, bookRepository)
        assertThat(savedBook).isNotNull()

        val recalledBook = bookRepository.findByIdOrNull(BOOK_A_ISBN)
        assertThat(recalledBook).isNotNull()
        assertThat(recalledBook).isEqualTo(savedBook)
        assertThat(isCreated).isTrue()
    }

    @Test
    fun `test that createUpdate throws successfully update book in the database`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, saveAuthor))
        assertThat(savedBook).isNotNull()

        val authorSummary = AuthorSummary(id=savedAuthor!!)
        val bookRepository = testBookSummaryA(BOOK_A_ISBN, authorSummary)
        val (ipdateBook, isCreated) = underTest.createUpdate(BOOK_A_ISBN, bookRepository)
        assertThat(savedBook).isNotNull()

        val recalledBook = bookRepository.findByIdOrNull(BOOK_A_ISBN)
        assertThat(recalledBook).isNotNull()
        assertThat(isCreated).isFalse()
    }

    @Test
    fun `test that list returns an empty list when no book in the database`(){
        val result = underTest.list()
        assertThat(result).isEmpty()
    }

    @Test
    fun `test that list returns books list when no book in the database`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, savedAuthor))
        assertThat(savedBook).isNotNull()

        val result = underTest.list()
        assertThat(result).hasSize(1)
        assertThat(result.get[0]).isEqualTo(savedAuthor)
    }

    @Test
    fun `test that list returns no books when the author ID does not match`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, savedAuthor))
        assertThat(savedBook).isNotNull()

        val result = underTest.list(authorId = savedAuthor.id!! + 1)
        assertThat(result).hasSize(0)
    }

    @Test
    fun `test that list returns books when the author ID does match`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, savedAuthor))
        assertThat(savedBook).isNotNull()

        val result = underTest.list(authorId = savedAuthor.id)
        assertThat(result).hasSize(1)
        assertThat(result[0]).isEqualTo(savedBook)
    }

    @Test
    fun `test that list returns books when the author ID does match`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, savedAuthor))
        assertThat(savedBook).isNotNull()

        val result = underTest.list(authorId = savedAuthor.id)
        assertThat(result).hasSize(1)
        assertThat(result[0]).isEqualTo(savedBook)
    }

    @Test
    fun `test that get returns null when book not found in the database`(){
        val result = underTest.get(BOOK_A_ISBN)
        assertThat(result).isNull()
    }

    @Test
    fun `test that get returns book when the book is found in the database`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, savedAuthor))
        assertThat(savedBook).isNotNull()

        val result = underTest.get(savedAuthor.isbn)
        assertThat(result).isEqualTo(savedBook)
    }
    @Test
    fun `test that package throws IllegalStateException when the book does not exist in the database`(){
        assertThrows<IllegalStateException> {
        val bookUpdateRequest = BookUpdateRequest(
        title = "A new title"
        )
        underTest.partialUpdate(BOOK_A_ISBN, bookUpdateRequest)
        }
    }

    @Test
    fun `test that partialUpdate updates the title of an existing book`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, savedAuthor))
        assertThat(savedBook).isNotNull()

        val newTitle = "A new title"
        val bookUpdateRequest = BookUpdateRequest(
            title = newTitle
        )

        underTest.partialUpdate(BOOK_A_ISBN, bookUpdateRequest)
        assertThat(result.title).isEqualTo(newTitle)
    }

    @Test
    fun `test that partialUpdate updates the description of an existing book`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, savedAuthor))
        assertThat(savedBook).isNotNull()

        val newDescription = "A new description"
        val bookUpdateRequest = BookUpdateRequest(
            description = newDescription
        )
        underTest.partialUpdate(BOOK_A_ISBN, bookUpdateRequest)
        assertThat(result.description).isEqualTo(newDescription)
    }

    @Test
    fun `test that delete successfully delete a book in the database`(){
        val savedAuthor = authorRepository.save(testAuthorEntityA())
        assertThat(savedAuthor).isNotNull()

        val savedBook = bookRepository.save(testAuthorEntityA(BOOK_A_ISBN, savedAuthor))
        assertThat(savedBook).isNotNull()
        
        underTest.delete(BOOK_A_ISBN)

        val result = bookRepository.findByIdOrNull(BOOK_A_ISBN)
        assertThat(result).isNotNull()
    }

    @Test
    fun `test that delete successfully delete a book that does not exist in the database`(){
        underTest.delete(BOOK_A_ISBN)

        val result = bookRepository.findByIdOrNull(BOOK_A_ISBN)
        assertThat(result).isNull()
    }
}
```


## 参考サイト
[How to build a REAL Webapp with Kotlin & Spring Boot](https://www.youtube.com/watch?v=7iJ0NWRaWic&t=192s)