diff --git a/http/nodes.http b/http/nodes.http new file mode 100644 index 0000000..87a6d77 --- /dev/null +++ b/http/nodes.http @@ -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 diff --git a/src/main/kotlin/ltd/hlaeja/controller/NodesController.kt b/src/main/kotlin/ltd/hlaeja/controller/NodesController.kt new file mode 100644 index 0000000..b36ad3b --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/controller/NodesController.kt @@ -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 = service.getNodes((page - 1) * show, show) + .map { it.toNodesResponse() } +} diff --git a/src/main/kotlin/ltd/hlaeja/repository/NodeRepository.kt b/src/main/kotlin/ltd/hlaeja/repository/NodeRepository.kt index 53a3ecd..75b8521 100644 --- a/src/main/kotlin/ltd/hlaeja/repository/NodeRepository.kt +++ b/src/main/kotlin/ltd/hlaeja/repository/NodeRepository.kt @@ -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 { @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 } diff --git a/src/main/kotlin/ltd/hlaeja/service/NodeService.kt b/src/main/kotlin/ltd/hlaeja/service/NodeService.kt index d84da2b..4d084fe 100644 --- a/src/main/kotlin/ltd/hlaeja/service/NodeService.kt +++ b/src/main/kotlin/ltd/hlaeja/service/NodeService.kt @@ -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 = nodeRepository.findAll(page, show) } diff --git a/src/main/kotlin/ltd/hlaeja/util/Mapping.kt b/src/main/kotlin/ltd/hlaeja/util/Mapping.kt index 43c1c99..39348f2 100644 --- a/src/main/kotlin/ltd/hlaeja/util/Mapping.kt +++ b/src/main/kotlin/ltd/hlaeja/util/Mapping.kt @@ -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, +) diff --git a/src/test-integration/kotlin/ltd/hlaeja/controller/NodesEndpoint.kt b/src/test-integration/kotlin/ltd/hlaeja/controller/NodesEndpoint.kt new file mode 100644 index 0000000..3ea1064 --- /dev/null +++ b/src/test-integration/kotlin/ltd/hlaeja/controller/NodesEndpoint.kt @@ -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>() + .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>() + .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>() + .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 + } +} diff --git a/src/test/kotlin/ltd/hlaeja/controller/NodesControllerTest.kt b/src/test/kotlin/ltd/hlaeja/controller/NodesControllerTest.kt new file mode 100644 index 0000000..ef430e1 --- /dev/null +++ b/src/test/kotlin/ltd/hlaeja/controller/NodesControllerTest.kt @@ -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) + } +}