add nodes endpoint

- add NodesController
  - add NodesEndpoint
  - add NodesControllerTest
  - add NodesController
  - add nodes.http

- add NodeEntity toNodesResponse in Mapping.kt

- add getNodes to NodeService

- add findAll to NodeRepository
This commit is contained in:
2025-08-17 23:12:27 +02:00
committed by swordsteel
parent 19aa9c8b6b
commit 119d14eb46
7 changed files with 232 additions and 0 deletions

8
http/nodes.http Normal file
View File

@@ -0,0 +1,8 @@
### get all types
GET {{hostname}}/nodes
### get all types
GET {{hostname}}/nodes/page-1
### get all types
GET {{hostname}}/nodes/page-1/show-2

View File

@@ -0,0 +1,30 @@
package ltd.hlaeja.controller
import jakarta.validation.constraints.Min
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import ltd.hlaeja.library.deviceRegistry.Nodes
import ltd.hlaeja.service.NodeService
import ltd.hlaeja.util.Pagination.DEFAULT_PAGE
import ltd.hlaeja.util.Pagination.DEFAULT_SIZE
import ltd.hlaeja.util.toNodesResponse
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RestController
@RestController
class NodesController(
private val service: NodeService,
) {
@GetMapping(
"/nodes",
"/nodes/page-{page}",
"/nodes/page-{page}/show-{show}",
)
suspend fun getNodes(
@PathVariable(required = false) @Min(1) page: Int = DEFAULT_PAGE,
@PathVariable(required = false) @Min(1) show: Int = DEFAULT_SIZE,
): Flow<Nodes.Response> = service.getNodes((page - 1) * show, show)
.map { it.toNodesResponse() }
}

View File

@@ -1,9 +1,11 @@
package ltd.hlaeja.repository
import java.util.UUID
import kotlinx.coroutines.flow.Flow
import ltd.hlaeja.entity.NodeEntity
import org.springframework.data.r2dbc.repository.Query
import org.springframework.data.repository.kotlin.CoroutineCrudRepository
import org.springframework.data.repository.query.Param
import org.springframework.stereotype.Repository
@Repository
@@ -11,4 +13,10 @@ interface NodeRepository : CoroutineCrudRepository<NodeEntity, UUID> {
@Query("SELECT * FROM nodes WHERE device = :device")
suspend fun findByDevice(device: UUID): NodeEntity?
@Query("SELECT * FROM nodes LIMIT :limit OFFSET :offset")
suspend fun findAll(
@Param("offset") offset: Int,
@Param("limit") limit: Int,
): Flow<NodeEntity>
}

View File

@@ -2,6 +2,7 @@ package ltd.hlaeja.service
import io.github.oshai.kotlinlogging.KotlinLogging
import java.util.UUID
import kotlinx.coroutines.flow.Flow
import ltd.hlaeja.entity.NodeEntity
import ltd.hlaeja.repository.NodeRepository
import org.springframework.dao.DataIntegrityViolationException
@@ -29,4 +30,9 @@ class NodeService(
device: UUID,
): NodeEntity = nodeRepository.findByDevice(device)
?: throw ResponseStatusException(NOT_FOUND)
suspend fun getNodes(
page: Int,
show: Int,
): Flow<NodeEntity> = nodeRepository.findAll(page, show)
}

View File

@@ -12,6 +12,7 @@ import ltd.hlaeja.library.deviceRegistry.Device
import ltd.hlaeja.library.deviceRegistry.Devices
import ltd.hlaeja.library.deviceRegistry.Identity
import ltd.hlaeja.library.deviceRegistry.Node
import ltd.hlaeja.library.deviceRegistry.Nodes
import ltd.hlaeja.library.deviceRegistry.Type
import ltd.hlaeja.library.deviceRegistry.Types
import org.springframework.http.HttpStatus.EXPECTATION_FAILED
@@ -75,3 +76,11 @@ fun DeviceEntity.toDevicesResponse(): Devices.Response = Devices.Response(
type,
timestamp,
)
fun NodeEntity.toNodesResponse(): Nodes.Response = Nodes.Response(
id ?: throw ResponseStatusException(EXPECTATION_FAILED),
timestamp,
client,
device,
name,
)

View File

@@ -0,0 +1,113 @@
package ltd.hlaeja.controller
import ltd.hlaeja.library.deviceRegistry.Nodes
import ltd.hlaeja.test.container.PostgresTestContainer
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.SoftAssertions
import org.assertj.core.api.junit.jupiter.InjectSoftAssertions
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody
@PostgresTestContainer
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ExtendWith(SoftAssertionsExtension::class)
class NodesEndpoint {
@InjectSoftAssertions
lateinit var softly: SoftAssertions
@LocalServerPort
var port: Int = 0
lateinit var webClient: WebTestClient
@BeforeEach
fun setup() {
webClient = WebTestClient.bindToServer().baseUrl("http://localhost:$port").build()
}
@Test
fun `get nodes default - success`() {
// when
val result = webClient.get().uri("/nodes").exchange()
// then
result.expectStatus().isOk()
.expectBody<List<Nodes.Response>>()
.consumeWith {
assertThat(it.responseBody?.size).isEqualTo(3)
}
}
@ParameterizedTest
@CsvSource(
value = [
"1,3",
"2,0",
],
)
fun `get nodes by page - success`(page: Int, expected: Int) {
// when
val result = webClient.get().uri("/nodes/page-$page").exchange()
// then
result.expectStatus().isOk()
.expectBody<List<Nodes.Response>>()
.consumeWith {
assertThat(it.responseBody?.size).isEqualTo(expected)
}
}
@Test
fun `get nodes by pages - fail`() {
// when
val result = webClient.get().uri("/nodes/page-0").exchange()
// then
result.expectStatus().isBadRequest
}
@ParameterizedTest
@CsvSource(
value = [
"1,2,2",
"2,2,1",
"3,2,0",
],
)
fun `get nodes by page and show - success`(page: Int, show: Int, expected: Int) {
// when
val result = webClient.get().uri("/nodes/page-$page/show-$show").exchange()
// then
result.expectStatus().isOk()
.expectBody<List<Nodes.Response>>()
.consumeWith {
assertThat(it.responseBody?.size).isEqualTo(expected)
}
}
@ParameterizedTest
@CsvSource(
value = [
"0,1",
"1,0",
],
)
fun `get nodes by page and show - fail`(page: Int, show: Int) {
// when
val result = webClient.get().uri("/nodes/page-$page/show-$show").exchange()
// then
result.expectStatus().isBadRequest
}
}

View File

@@ -0,0 +1,58 @@
package ltd.hlaeja.controller
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
import java.util.UUID
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.single
import kotlinx.coroutines.test.runTest
import ltd.hlaeja.entity.NodeEntity
import ltd.hlaeja.service.NodeService
import ltd.hlaeja.test.isEqualToUuid
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class NodesControllerTest {
companion object {
const val NAME: String = "My Device"
const val NIL_UUID: String = "00000000-0000-0000-0000-000000000000"
val id: UUID = UUID.fromString(NIL_UUID)
val client: UUID = UUID.fromString(NIL_UUID)
val device: UUID = UUID.fromString(NIL_UUID)
val timestamp: ZonedDateTime = ZonedDateTime.of(LocalDateTime.of(2000, 1, 1, 0, 0, 0, 1), ZoneId.of("UTC"))
}
val service: NodeService = mockk()
lateinit var controller: NodesController
@BeforeEach
fun setUp() {
controller = NodesController(service)
}
@Test
fun `get all nodes`() = runTest {
// given
coEvery {
service.getNodes(any(), any())
} returns flowOf(NodeEntity(id, timestamp, client, device, NAME))
// when
val response = controller.getNodes().single()
// then
coVerify(exactly = 1) { service.getNodes(0, 25) }
assertThat(response.id).isEqualToUuid(NIL_UUID)
assertThat(response.timestamp).isEqualTo(timestamp)
assertThat(response.client).isEqualToUuid(NIL_UUID)
assertThat(response.device).isEqualToUuid(NIL_UUID)
assertThat(response.name).isEqualTo(NAME)
}
}