Skip to content

Commit

Permalink
MLT-0042 Order GET endpoint (#30)
Browse files Browse the repository at this point in the history
* Fixed duplicate order creation, removed Enum values

* Implement GET endpoint

* Swaggerdoc

* Add comments to OrderService

* Check authentication when ordering

Currently breaks tests

* Better handling of client name for order tests

* Fix JWT mocking

* Sneakily improve SynQ createOrder

* Move throwIfInvalidClientName into HostName

---------

Co-authored-by: Daniel Aaron Salwerowicz <[email protected]>
  • Loading branch information
anotheroneofthese and MormonJesus69420 authored Aug 29, 2024
1 parent 8358367 commit 4be86e1
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 53 deletions.
20 changes: 15 additions & 5 deletions src/main/kotlin/no/nb/mlt/wls/core/data/HostName.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
package no.nb.mlt.wls.core.data

enum class HostName(private val hostName: String) {
AXIELL("Axiell");
import org.springframework.http.HttpStatus
import org.springframework.web.server.ResponseStatusException

override fun toString(): String {
return hostName
}
enum class HostName {
AXIELL
}

fun throwIfInvalidClientName(
clientName: String,
hostName: HostName
) {
if (clientName.uppercase() == hostName.name) return
throw ResponseStatusException(
HttpStatus.FORBIDDEN,
"You do not have access to view resources owned by $hostName"
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@ import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
import io.swagger.v3.oas.annotations.tags.Tag
import no.nb.mlt.wls.core.data.HostName
import no.nb.mlt.wls.order.model.Order
import no.nb.mlt.wls.order.payloads.ApiOrderPayload
import no.nb.mlt.wls.order.service.OrderService
import org.springframework.http.ResponseEntity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
Expand Down Expand Up @@ -70,6 +76,45 @@ class OrderController(val orderService: OrderService) {
)
@PostMapping("/order/batch/create")
suspend fun createOrder(
@AuthenticationPrincipal jwt: JwtAuthenticationToken,
@RequestBody payload: ApiOrderPayload
): ResponseEntity<ApiOrderPayload> = orderService.createOrder(payload)
): ResponseEntity<ApiOrderPayload> = orderService.createOrder(jwt.name, payload)

@Operation(
summary = "Gets an order from the storage system",
description = "Checks if a specified order exists within Hermes WLS."
)
@ApiResponses(
ApiResponse(
responseCode = "200",
description = "Order with given 'hostName' and 'hostOrderId' exists in the system.",
content = [
Content(
mediaType = "application/json",
schema = Schema(implementation = ApiOrderPayload::class)
)
]
),
ApiResponse(
responseCode = "400",
description = """Some field was invalid in your request. The error message contains information about the invalid fields.""",
content = [Content(schema = Schema())]
),
ApiResponse(
responseCode = "401",
description = "Client sending the request is not authorized to request orders.",
content = [Content(schema = Schema())]
),
ApiResponse(
responseCode = "403",
description = "A valid 'Authorization' header is missing from the request.",
content = [Content(schema = Schema())]
)
)
@GetMapping("/order/{hostName}/{hostOrderId}")
suspend fun getOrder(
@AuthenticationPrincipal jwt: JwtAuthenticationToken,
@PathVariable("hostName") hostName: HostName,
@PathVariable("hostOrderId") hostOrderId: String
): ResponseEntity<Order> = orderService.getOrder(jwt.name, hostName, hostOrderId)
}
36 changes: 12 additions & 24 deletions src/main/kotlin/no/nb/mlt/wls/order/model/Order.kt
Original file line number Diff line number Diff line change
Expand Up @@ -88,32 +88,20 @@ data class OrderReceiver(
val phoneNumber: String?
)

enum class OrderStatus(private val status: String) {
NOT_STARTED("Not started"),
IN_PROGRESS("In progress"),
COMPLETED("Completed"),
DELETED("Deleted");

override fun toString(): String {
return status
}
enum class OrderStatus {
NOT_STARTED,
IN_PROGRESS,
COMPLETED,
DELETED
}

enum class OrderLineStatus(private val status: String) {
NOT_STARTED("Not started"),
PICKED("Picked"),
FAILED("Failed");

override fun toString(): String {
return status
}
enum class OrderLineStatus {
NOT_STARTED,
PICKED,
FAILED
}

enum class OrderType(private val type: String) {
LOAN("Loan"),
DIGITIZATION("Digitization") ;

override fun toString(): String {
return type
}
enum class OrderType {
LOAN,
DIGITIZATION
}
10 changes: 2 additions & 8 deletions src/main/kotlin/no/nb/mlt/wls/order/payloads/SynqOrderPayload.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package no.nb.mlt.wls.order.payloads

import com.fasterxml.jackson.annotation.JsonFormat
import com.fasterxml.jackson.annotation.JsonValue
import jakarta.validation.constraints.Min
import no.nb.mlt.wls.core.data.HostName
import no.nb.mlt.wls.core.data.synq.SynqOwner
Expand Down Expand Up @@ -32,13 +31,8 @@ data class SynqOrderPayload(
val quantityOrdered: Double
)

enum class SynqOrderType(private val type: String) {
STANDARD("Standard");

@JsonValue
override fun toString(): String {
return type
}
enum class SynqOrderType {
STANDARD
}
}

Expand Down
33 changes: 31 additions & 2 deletions src/main/kotlin/no/nb/mlt/wls/order/service/OrderService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package no.nb.mlt.wls.order.service
import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.reactive.awaitSingle
import kotlinx.coroutines.reactor.awaitSingleOrNull
import no.nb.mlt.wls.core.data.HostName
import no.nb.mlt.wls.core.data.throwIfInvalidClientName
import no.nb.mlt.wls.order.model.Order
import no.nb.mlt.wls.order.payloads.ApiOrderPayload
import no.nb.mlt.wls.order.payloads.toApiOrderPayload
import no.nb.mlt.wls.order.payloads.toOrder
Expand All @@ -11,6 +14,7 @@ import no.nb.mlt.wls.order.repository.OrderRepository
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Service
import org.springframework.web.server.ResponseStatusException
import org.springframework.web.server.ServerErrorException
import org.springframework.web.server.ServerWebInputException
import java.time.Duration
Expand All @@ -20,11 +24,18 @@ private val logger = KotlinLogging.logger {}

@Service
class OrderService(val db: OrderRepository, val synqService: SynqOrderService) {
suspend fun createOrder(payload: ApiOrderPayload): ResponseEntity<ApiOrderPayload> {
/**
* Creates an order within the WLS database, and sends it to the appropriate storage systems
*/
suspend fun createOrder(
clientName: String,
payload: ApiOrderPayload
): ResponseEntity<ApiOrderPayload> {
throwIfInvalidClientName(clientName, payload.hostName)
throwIfInvalidPayload(payload)

val existingOrder =
db.findByHostNameAndHostOrderId(payload.hostName, payload.orderId)
db.findByHostNameAndHostOrderId(payload.hostName, payload.hostOrderId)
.timeout(Duration.ofSeconds(8))
.onErrorMap {
if (it is TimeoutException) {
Expand Down Expand Up @@ -65,6 +76,24 @@ class OrderService(val db: OrderRepository, val synqService: SynqOrderService) {
return ResponseEntity.status(HttpStatus.CREATED).body(order.toApiOrderPayload())
}

/**
* Gets an order from the WLS database
*/
suspend fun getOrder(
clientName: String,
hostName: HostName,
hostOrderId: String
): ResponseEntity<Order> {
throwIfInvalidClientName(clientName, hostName)
val order =
db.findByHostNameAndHostOrderId(hostName, hostOrderId)
.awaitSingleOrNull()
if (order != null) {
return ResponseEntity.ok(order)
}
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Order with id $hostOrderId from $hostName was not found")
}

private fun throwIfInvalidPayload(payload: ApiOrderPayload) {
if (payload.orderId.isBlank()) {
throw ServerWebInputException("The order's orderId is required, and can not be blank")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import no.nb.mlt.wls.order.payloads.SynqOrder
import no.nb.mlt.wls.order.payloads.SynqOrderPayload
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Service
import org.springframework.web.reactive.function.BodyInserters
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.WebClientResponseException
import org.springframework.web.server.ResponseStatusException
import org.springframework.web.server.ServerErrorException
import reactor.core.publisher.Mono
import java.net.URI
Expand All @@ -36,8 +38,11 @@ class SynqOrderService(
.retrieve()
.toEntity(SynqError::class.java)
.onErrorResume(WebClientResponseException::class.java) { error ->
val errorText = error.getResponseBodyAs(SynqError::class.java)?.errorText
if (errorText != null && errorText.contains("Duplicate order")) {
val synqError = error.getResponseBodyAs(SynqError::class.java) ?: throw createServerError(error)
if (synqError.errorCode == 1037 || synqError.errorCode == 1029) {
throw ResponseStatusException(HttpStatus.NOT_FOUND, synqError.errorText)
}
if (synqError.errorText.contains("Duplicate order")) {
Mono.error(DuplicateOrderException(error))
} else {
Mono.error(error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import no.nb.mlt.wls.order.model.ProductLine
import no.nb.mlt.wls.order.payloads.ApiOrderPayload
import no.nb.mlt.wls.order.payloads.toOrder
import no.nb.mlt.wls.order.repository.OrderRepository
import no.nb.mlt.wls.order.service.OrderService
import no.nb.mlt.wls.order.service.SynqOrderService
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
Expand All @@ -30,11 +29,14 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
import org.springframework.context.ApplicationContext
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.security.test.context.support.WithMockUser
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockJwt
import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody
import java.net.URI
Expand All @@ -46,18 +48,22 @@ import java.net.URI
@EnableMongoRepositories("no.nb.mlt.wls")
@SpringBootTest(webEnvironment = RANDOM_PORT)
class OrderControllerTest(
@Autowired val repository: OrderRepository
@Autowired val repository: OrderRepository,
@Autowired val applicationContext: ApplicationContext
) {
@MockkBean
private lateinit var synqOrderService: SynqOrderService

private lateinit var webTestClient: WebTestClient

val clientName: String = HostName.AXIELL.name

@BeforeEach
fun setUp() {
webTestClient =
WebTestClient
.bindToController(OrderController(OrderService(repository, synqOrderService)))
.bindToApplicationContext(applicationContext)
.apply(springSecurity())
.configureClient()
.baseUrl("/v1/order")
.build()
Expand All @@ -66,7 +72,6 @@ class OrderControllerTest(
}

@Test
@WithMockUser
fun `createOrder with valid payload creates order`() =
runTest {
coEvery {
Expand All @@ -75,6 +80,7 @@ class OrderControllerTest(

webTestClient
.mutateWith(csrf())
.mutateWith(mockJwt().jwt { it.subject(clientName) })
.post()
.uri("/batch/create")
.accept(MediaType.APPLICATION_JSON)
Expand All @@ -91,10 +97,10 @@ class OrderControllerTest(
}

@Test
@WithMockUser
fun `createOrder with duplicate payload returns OK`() {
webTestClient
.mutateWith(csrf())
.mutateWith(mockJwt().jwt { it.subject(clientName) })
.post()
.uri("/batch/create")
.accept(MediaType.APPLICATION_JSON)
Expand All @@ -110,10 +116,10 @@ class OrderControllerTest(
}

@Test
@WithMockUser
fun `createOrder payload with different data but same ID returns DB entry`() {
webTestClient
.mutateWith(csrf())
.mutateWith(mockJwt().jwt { it.subject(clientName) })
.post()
.uri("/batch/create")
.accept(MediaType.APPLICATION_JSON)
Expand All @@ -137,6 +143,7 @@ class OrderControllerTest(

webTestClient
.mutateWith(csrf())
.mutateWith(mockJwt().jwt { it.subject(clientName) })
.post()
.uri("/batch/create")
.accept(MediaType.APPLICATION_JSON)
Expand All @@ -155,6 +162,7 @@ class OrderControllerTest(

webTestClient
.mutateWith(csrf())
.mutateWith(mockJwt().jwt { it.subject(clientName) })
.post()
.uri("/batch/create")
.accept(MediaType.APPLICATION_JSON)
Expand Down
Loading

0 comments on commit 4be86e1

Please sign in to comment.