Skip to content

Commit

Permalink
Implement journal entry creation for goal (#15)
Browse files Browse the repository at this point in the history
Resolved: #2
  • Loading branch information
zionhann authored Jul 30, 2024
1 parent 5f111cb commit 79c0f2a
Show file tree
Hide file tree
Showing 11 changed files with 238 additions and 2 deletions.
2 changes: 0 additions & 2 deletions src/main/kotlin/com/likelionhgu/stepper/StepperApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ package com.likelionhgu.stepper

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.data.jpa.repository.config.EnableJpaAuditing

@SpringBootApplication
@EnableJpaAuditing
class StepperApplication

fun main(args: Array<String>) {
Expand Down
9 changes: 9 additions & 0 deletions src/main/kotlin/com/likelionhgu/stepper/common/JpaConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.likelionhgu.stepper.common

import org.springframework.context.annotation.Configuration
import org.springframework.data.jpa.repository.config.EnableJpaAuditing

@Configuration
@EnableJpaAuditing
class JpaConfig {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.likelionhgu.stepper.goal

class GoalNotFoundException(message: String) : RuntimeException(message) {

}
13 changes: 13 additions & 0 deletions src/main/kotlin/com/likelionhgu/stepper/goal/GoalService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.likelionhgu.stepper.goal.request.GoalRequest
import com.likelionhgu.stepper.member.MemberRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import kotlin.jvm.optionals.getOrNull

@Service
@Transactional
Expand Down Expand Up @@ -34,4 +35,16 @@ class GoalService(

return goalRepository.findAllByMember(member, sortType.sort())
}

/**
* Retrieves the detailed information of a goal based on its ID.
*
* @param goalId The ID of the goal.
* @return Goal The `Goal` entity corresponding to the provided ID.
* @throws GoalNotFoundException if no goal is found with the provided ID.
*/
fun goalInfo(goalId: String): Goal {
return goalRepository.findById(goalId.toLong()).getOrNull()
?: throw GoalNotFoundException("The goal with the id \"$goalId\" does not exist")
}
}
35 changes: 35 additions & 0 deletions src/main/kotlin/com/likelionhgu/stepper/journal/Journal.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.likelionhgu.stepper.journal

import com.likelionhgu.stepper.goal.Goal
import com.likelionhgu.stepper.member.Member
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.FetchType
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.JoinColumn
import jakarta.persistence.ManyToOne

@Entity
class Journal(

@Column
var title: String,

@Column(length = 2048)
var content: String,

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
val member: Member,

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "goal_id")
val goal: Goal
) {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val journalId = 0L
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.likelionhgu.stepper.journal

import com.likelionhgu.stepper.goal.GoalService
import com.likelionhgu.stepper.journal.request.JournalRequest
import com.likelionhgu.stepper.member.MemberService
import com.likelionhgu.stepper.security.oauth2.CommonOAuth2Attribute
import jakarta.validation.Valid
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController

@RestController
class JournalController(
private val memberService: MemberService,
private val journalService: JournalService,
private val goalService: GoalService
) {

@PostMapping("/v1/goals/{goalId}/journals")
fun writeJournal(
@AuthenticationPrincipal user: CommonOAuth2Attribute,
@PathVariable goalId: String,
@Valid @RequestBody journalRequest: JournalRequest
): ResponseEntity<Unit> {
val member = memberService.memberInfo(user.oauth2UserId)
val goal = goalService.goalInfo(goalId)
journalService.writeJournal(member, goal, journalRequest)

return ResponseEntity.status(HttpStatus.CREATED).build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.likelionhgu.stepper.journal;

import org.springframework.data.jpa.repository.JpaRepository

interface JournalRepository : JpaRepository<Journal, Long> {
}
29 changes: 29 additions & 0 deletions src/main/kotlin/com/likelionhgu/stepper/journal/JournalService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.likelionhgu.stepper.journal

import com.likelionhgu.stepper.goal.Goal
import com.likelionhgu.stepper.journal.request.JournalRequest
import com.likelionhgu.stepper.member.Member
import org.springframework.stereotype.Service

@Service
class JournalService(
private val journalRepository: JournalRepository
) {

/**
* Write a journal entry for a goal.
*
* @param member The member who is the author of the journal entry.
* @param goal The goal for which the journal entry is being written.
*/
fun writeJournal(member: Member, goal: Goal, request: JournalRequest): Long {
val journal = Journal(
title = request.title!!,
content = request.content.orEmpty(),
member = member,
goal = goal
).let(journalRepository::save)

return journal.journalId
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.likelionhgu.stepper.journal.request

import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.NotNull

data class JournalRequest(

@field:NotNull(message = "title is required")
@field:NotBlank(message = "title cannot be blank")
val title: String?,

@field:NotNull(message = "content cannot be null")
val content: String?
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.likelionhgu.stepper.journal

import com.likelionhgu.stepper.goal.GoalService
import com.likelionhgu.stepper.member.MemberService
import com.ninjasquad.springmockk.MockkBean
import io.kotest.core.spec.style.BehaviorSpec
import io.mockk.every
import io.mockk.mockk
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.http.MediaType
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oauth2Login
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.post

@WebMvcTest(JournalController::class)
class JournalControllerTest(
private val mockMvc: MockMvc,

@MockkBean
private val journalService: JournalService,

@MockkBean
private val memberService: MemberService,

@MockkBean
private val goalService: GoalService
) : BehaviorSpec({

given("A member who has created a goal") {
`when`("the member writes the journal entry with a title and content") {
every { memberService.memberInfo(any()) } returns mockk()
every { goalService.goalInfo(any()) } returns mockk()
every { journalService.writeJournal(any(), any(), any()) } returns 1L
}
then("the request should be successful") {
mockMvc.post("/v1/goals/1/journals") {
with(csrf())
with(oauth2Login())
contentType = MediaType.APPLICATION_JSON
content = """{"title": "title", "content": "content"}"""
}
.andDo { print() }
.andExpect { status { isCreated() } }
}

`when`("the member writes the journal entry without a title") {
every { memberService.memberInfo(any()) } returns mockk()
every { goalService.goalInfo(any()) } returns mockk()
every { journalService.writeJournal(any(), any(), any()) } returns 1L
}
then("the request should be bad request") {
mockMvc.post("/v1/goals/1/journals") {
with(csrf())
with(oauth2Login())
contentType = MediaType.APPLICATION_JSON
content = """{"title": null, "content": "content"}"""
}
.andDo { print() }
.andExpect { status { isBadRequest() } }
}
}
}) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.likelionhgu.stepper.journal

import com.likelionhgu.stepper.journal.request.JournalRequest
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.extensions.spring.SpringExtension
import io.kotest.matchers.shouldNotBe
import io.mockk.every
import io.mockk.mockk

class JournalServiceTest : BehaviorSpec({
val journalRepository = mockk<JournalRepository>()
val journalService = JournalService(journalRepository)

given("A member who has created a goal") {
`when`("the member writes the journal entry") {
every { journalRepository.save(any<Journal>()) } returns Journal("title", "content", mockk(), mockk())
val request = JournalRequest("title", "content")

then("the journal entry should be saved") {
val journalId = journalService.writeJournal(mockk(), mockk(), request)

journalId shouldNotBe null
}
}
}
}) {
override fun extensions() = listOf(SpringExtension)
}

0 comments on commit 79c0f2a

Please sign in to comment.