diff --git a/gradle.properties b/gradle.properties index 0a6c777..fb9ab10 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ kotlin.code.style=official version=0.7.0-SNAPSHOT -catalog=0.11.0 +catalog=0.12.0-SNAPSHOT container.port.host=9010 diff --git a/http/devices.http b/http/devices.http new file mode 100644 index 0000000..ee9a648 --- /dev/null +++ b/http/devices.http @@ -0,0 +1,8 @@ +### get all types +GET {{hostname}}/devices + +### get all types +GET {{hostname}}/devices/page-1 + +### get all types +GET {{hostname}}/devices/page-1/show-2 diff --git a/src/main/kotlin/ltd/hlaeja/controller/DevicesController.kt b/src/main/kotlin/ltd/hlaeja/controller/DevicesController.kt new file mode 100644 index 0000000..ed50e36 --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/controller/DevicesController.kt @@ -0,0 +1,33 @@ +package ltd.hlaeja.controller + +import jakarta.validation.constraints.Min +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import ltd.hlaeja.library.deviceRegistry.Devices +import ltd.hlaeja.service.DeviceService +import ltd.hlaeja.util.toDevicesResponse +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RestController + +@RestController +class DevicesController( + private val deviceService: DeviceService, +) { + + companion object { + const val DEFAULT_PAGE: Int = 1 + const val DEFAULT_SIZE: Int = 25 + } + + @GetMapping( + "/devices", + "/devices/page-{page}", + "/devices/page-{page}/show-{show}", + ) + suspend fun getDevices( + @PathVariable(required = false) @Min(1) page: Int = DEFAULT_PAGE, + @PathVariable(required = false) @Min(1) show: Int = DEFAULT_SIZE, + ): Flow = deviceService.getDevices((page - 1) * show, show) + .map { it.toDevicesResponse() } +} diff --git a/src/main/kotlin/ltd/hlaeja/controller/TypesController.kt b/src/main/kotlin/ltd/hlaeja/controller/TypesController.kt index 65fb8c9..d9b1072 100644 --- a/src/main/kotlin/ltd/hlaeja/controller/TypesController.kt +++ b/src/main/kotlin/ltd/hlaeja/controller/TypesController.kt @@ -27,7 +27,7 @@ class TypesController( "/types/filter-{filter}/page-{page}", "/types/filter-{filter}/page-{page}/show-{show}", ) - fun getTypes( + suspend fun getTypes( @PathVariable(required = false) @Min(1) page: Int = DEFAULT_PAGE, @PathVariable(required = false) @Min(1) show: Int = DEFAULT_SIZE, @PathVariable(required = false) filter: String? = null, diff --git a/src/main/kotlin/ltd/hlaeja/repository/DeviceRepository.kt b/src/main/kotlin/ltd/hlaeja/repository/DeviceRepository.kt index 2a2440d..ed7eef3 100644 --- a/src/main/kotlin/ltd/hlaeja/repository/DeviceRepository.kt +++ b/src/main/kotlin/ltd/hlaeja/repository/DeviceRepository.kt @@ -1,9 +1,19 @@ package ltd.hlaeja.repository import java.util.UUID +import kotlinx.coroutines.flow.Flow import ltd.hlaeja.entity.DeviceEntity +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 -interface DeviceRepository : CoroutineCrudRepository +interface DeviceRepository : CoroutineCrudRepository { + + @Query("SELECT * FROM devices LIMIT :limit OFFSET :offset") + fun findAll( + @Param("offset") offset: Int, + @Param("limit") limit: Int, + ): Flow +} diff --git a/src/main/kotlin/ltd/hlaeja/repository/TypeRepository.kt b/src/main/kotlin/ltd/hlaeja/repository/TypeRepository.kt index 541e868..0d9ba7e 100644 --- a/src/main/kotlin/ltd/hlaeja/repository/TypeRepository.kt +++ b/src/main/kotlin/ltd/hlaeja/repository/TypeRepository.kt @@ -13,13 +13,13 @@ import org.springframework.stereotype.Repository interface TypeRepository : CoroutineCrudRepository { @Query("SELECT * FROM types ORDER BY name LIMIT :limit OFFSET :offset") - fun findAll( + suspend fun findAll( @Param("offset") offset: Int, @Param("limit") limit: Int, ): Flow @Query("SELECT * FROM types WHERE name ILIKE :filter ORDER BY name LIMIT :limit OFFSET :offset") - fun findAllContaining( + suspend fun findAllContaining( @Param("filter") filter: String, @Param("offset") offset: Int, @Param("limit") limit: Int, diff --git a/src/main/kotlin/ltd/hlaeja/service/DeviceService.kt b/src/main/kotlin/ltd/hlaeja/service/DeviceService.kt index 74304f4..f1e21c5 100644 --- a/src/main/kotlin/ltd/hlaeja/service/DeviceService.kt +++ b/src/main/kotlin/ltd/hlaeja/service/DeviceService.kt @@ -3,6 +3,7 @@ package ltd.hlaeja.service import io.github.oshai.kotlinlogging.KotlinLogging import java.time.ZonedDateTime import java.util.UUID +import kotlinx.coroutines.flow.Flow import ltd.hlaeja.entity.DeviceEntity import ltd.hlaeja.repository.DeviceRepository import org.springframework.dao.DataIntegrityViolationException @@ -31,4 +32,9 @@ class DeviceService( suspend fun getDevice(device: UUID): DeviceEntity = deviceRepository.findById(device) ?.also { log.debug { "Get device ${it.id}" } } ?: throw ResponseStatusException(NOT_FOUND) + + suspend fun getDevices( + page: Int, + show: Int, + ): Flow = deviceRepository.findAll(page, show) } diff --git a/src/main/kotlin/ltd/hlaeja/service/TypeService.kt b/src/main/kotlin/ltd/hlaeja/service/TypeService.kt index 2634ea7..4b4ff95 100644 --- a/src/main/kotlin/ltd/hlaeja/service/TypeService.kt +++ b/src/main/kotlin/ltd/hlaeja/service/TypeService.kt @@ -26,7 +26,7 @@ class TypeService( private val typeDescriptionRepository: TypeDescriptionRepository, ) { - fun getTypes( + suspend fun getTypes( page: Int, show: Int, filter: String?, diff --git a/src/main/kotlin/ltd/hlaeja/util/Mapping.kt b/src/main/kotlin/ltd/hlaeja/util/Mapping.kt index 508855b..43c1c99 100644 --- a/src/main/kotlin/ltd/hlaeja/util/Mapping.kt +++ b/src/main/kotlin/ltd/hlaeja/util/Mapping.kt @@ -2,13 +2,14 @@ package ltd.hlaeja.util import java.time.ZonedDateTime import java.util.UUID +import ltd.hlaeja.dto.TypeWithDescription import ltd.hlaeja.entity.DeviceEntity import ltd.hlaeja.entity.NodeEntity -import ltd.hlaeja.entity.TypeEntity -import ltd.hlaeja.dto.TypeWithDescription import ltd.hlaeja.entity.TypeDescriptionEntity +import ltd.hlaeja.entity.TypeEntity import ltd.hlaeja.jwt.service.PrivateJwtService 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.Type @@ -68,3 +69,9 @@ fun DeviceEntity.toDeviceResponse( type, jwtService.sign("device" to id), ) + +fun DeviceEntity.toDevicesResponse(): Devices.Response = Devices.Response( + id ?: throw ResponseStatusException(EXPECTATION_FAILED), + type, + timestamp, +) diff --git a/src/test-integration/kotlin/ltd/hlaeja/controller/DeviceEndpoint.kt b/src/test-integration/kotlin/ltd/hlaeja/controller/DeviceEndpoint.kt index eccf1d3..29d06d7 100644 --- a/src/test-integration/kotlin/ltd/hlaeja/controller/DeviceEndpoint.kt +++ b/src/test-integration/kotlin/ltd/hlaeja/controller/DeviceEndpoint.kt @@ -3,7 +3,7 @@ package ltd.hlaeja.controller import java.util.UUID import ltd.hlaeja.library.deviceRegistry.Device import ltd.hlaeja.test.compareToFile -import ltd.hlaeja.test.container.PostgresContainer +import ltd.hlaeja.test.container.PostgresTestContainer import ltd.hlaeja.test.isEqualToUuid import org.assertj.core.api.SoftAssertions import org.assertj.core.api.junit.jupiter.InjectSoftAssertions @@ -18,7 +18,7 @@ import org.springframework.boot.test.web.server.LocalServerPort import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.test.web.reactive.server.expectBody -@PostgresContainer +@PostgresTestContainer @SpringBootTest(webEnvironment = RANDOM_PORT) @ExtendWith(SoftAssertionsExtension::class) class DeviceEndpoint { diff --git a/src/test-integration/kotlin/ltd/hlaeja/controller/DevicesEndpoint.kt b/src/test-integration/kotlin/ltd/hlaeja/controller/DevicesEndpoint.kt new file mode 100644 index 0000000..8c156e1 --- /dev/null +++ b/src/test-integration/kotlin/ltd/hlaeja/controller/DevicesEndpoint.kt @@ -0,0 +1,105 @@ +package ltd.hlaeja.controller + +import ltd.hlaeja.library.deviceRegistry.Devices +import ltd.hlaeja.test.container.PostgresTestContainer +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +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) +class DevicesEndpoint { + + @LocalServerPort + var port: Int = 0 + + lateinit var webClient: WebTestClient + + @BeforeEach + fun setup() { + webClient = WebTestClient.bindToServer().baseUrl("http://localhost:$port").build() + } + + @Test + fun `get devices default - success`() { + // when + val result = webClient.get().uri("/devices").exchange() + + // then + result.expectStatus().isOk() + .expectBody>() + .consumeWith { + assertThat(it.responseBody?.size).isEqualTo(4) + } + } + + @ParameterizedTest + @CsvSource( + value = [ + "1,4", + "2,0", + ], + ) + fun `get devices by page - success`(page: Int, expected: Int) { + // when + val result = webClient.get().uri("/devices/page-$page").exchange() + + // then + result.expectStatus().isOk() + .expectBody>() + .consumeWith { + assertThat(it.responseBody?.size).isEqualTo(expected) + } + } + + @Test + fun `get devices by pages - fail`() { + // when + val result = webClient.get().uri("/devices/page-0").exchange() + + // then + result.expectStatus().isBadRequest + } + + @ParameterizedTest + @CsvSource( + value = [ + "1,2,2", + "2,2,2", + "3,2,0", + ], + ) + fun `get devices by page and show - success`(page: Int, show: Int, expected: Int) { + // when + val result = webClient.get().uri("/devices/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 devices by page and show - fail`(page: Int, show: Int) { + // when + val result = webClient.get().uri("/devices/page-$page/show-$show").exchange() + + // then + result.expectStatus().isBadRequest + } +} diff --git a/src/test-integration/kotlin/ltd/hlaeja/controller/IdentityEndpoint.kt b/src/test-integration/kotlin/ltd/hlaeja/controller/IdentityEndpoint.kt index 98f121a..7b6acba 100644 --- a/src/test-integration/kotlin/ltd/hlaeja/controller/IdentityEndpoint.kt +++ b/src/test-integration/kotlin/ltd/hlaeja/controller/IdentityEndpoint.kt @@ -2,7 +2,7 @@ package ltd.hlaeja.controller import java.util.UUID import ltd.hlaeja.library.deviceRegistry.Identity -import ltd.hlaeja.test.container.PostgresContainer +import ltd.hlaeja.test.container.PostgresTestContainer import ltd.hlaeja.test.isEqualToUuid import org.assertj.core.api.SoftAssertions import org.assertj.core.api.junit.jupiter.InjectSoftAssertions @@ -16,7 +16,7 @@ import org.springframework.boot.test.web.server.LocalServerPort import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.test.web.reactive.server.expectBody -@PostgresContainer +@PostgresTestContainer @SpringBootTest(webEnvironment = RANDOM_PORT) @ExtendWith(SoftAssertionsExtension::class) class IdentityEndpoint { diff --git a/src/test-integration/kotlin/ltd/hlaeja/controller/NodeEndpoint.kt b/src/test-integration/kotlin/ltd/hlaeja/controller/NodeEndpoint.kt index e40dfff..f4d59ef 100644 --- a/src/test-integration/kotlin/ltd/hlaeja/controller/NodeEndpoint.kt +++ b/src/test-integration/kotlin/ltd/hlaeja/controller/NodeEndpoint.kt @@ -2,7 +2,7 @@ package ltd.hlaeja.controller import java.util.UUID import ltd.hlaeja.library.deviceRegistry.Node -import ltd.hlaeja.test.container.PostgresContainer +import ltd.hlaeja.test.container.PostgresTestContainer import org.assertj.core.api.SoftAssertions import org.assertj.core.api.junit.jupiter.InjectSoftAssertions import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension @@ -17,7 +17,7 @@ import org.springframework.boot.test.web.server.LocalServerPort import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.test.web.reactive.server.expectBody -@PostgresContainer +@PostgresTestContainer @SpringBootTest(webEnvironment = RANDOM_PORT) @ExtendWith(SoftAssertionsExtension::class) class NodeEndpoint { diff --git a/src/test-integration/kotlin/ltd/hlaeja/controller/TypeEndpoint.kt b/src/test-integration/kotlin/ltd/hlaeja/controller/TypeEndpoint.kt index df44423..6650446 100644 --- a/src/test-integration/kotlin/ltd/hlaeja/controller/TypeEndpoint.kt +++ b/src/test-integration/kotlin/ltd/hlaeja/controller/TypeEndpoint.kt @@ -1,7 +1,7 @@ package ltd.hlaeja.controller import ltd.hlaeja.library.deviceRegistry.Type -import ltd.hlaeja.test.container.PostgresContainer +import ltd.hlaeja.test.container.PostgresTestContainer import ltd.hlaeja.test.isEqualToUuid import org.assertj.core.api.SoftAssertions import org.assertj.core.api.junit.jupiter.InjectSoftAssertions @@ -21,7 +21,7 @@ import org.springframework.http.MediaType.APPLICATION_JSON import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.test.web.reactive.server.expectBody -@PostgresContainer +@PostgresTestContainer @SpringBootTest(webEnvironment = RANDOM_PORT) @ExtendWith(SoftAssertionsExtension::class) class TypeEndpoint { diff --git a/src/test-integration/kotlin/ltd/hlaeja/controller/TypesEndpoint.kt b/src/test-integration/kotlin/ltd/hlaeja/controller/TypesEndpoint.kt index 064fb47..a6cbbc6 100644 --- a/src/test-integration/kotlin/ltd/hlaeja/controller/TypesEndpoint.kt +++ b/src/test-integration/kotlin/ltd/hlaeja/controller/TypesEndpoint.kt @@ -1,11 +1,10 @@ package ltd.hlaeja.controller import ltd.hlaeja.library.deviceRegistry.Types -import ltd.hlaeja.test.container.PostgresContainer +import ltd.hlaeja.test.container.PostgresTestContainer import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.CsvSource import org.springframework.boot.test.context.SpringBootTest @@ -14,9 +13,8 @@ import org.springframework.boot.test.web.server.LocalServerPort import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.test.web.reactive.server.expectBody -@PostgresContainer +@PostgresTestContainer @SpringBootTest(webEnvironment = RANDOM_PORT) -@TestInstance(TestInstance.Lifecycle.PER_CLASS) class TypesEndpoint { @LocalServerPort diff --git a/src/test-integration/resources/application.yml b/src/test-integration/resources/application.yml index 7ce3f7d..32c88c7 100644 --- a/src/test-integration/resources/application.yml +++ b/src/test-integration/resources/application.yml @@ -6,10 +6,3 @@ spring: url: r2dbc:postgresql://placeholder username: placeholder password: placeholder - -container: - postgres: - version: postgres:17 - init: postgres/schema.sql - before: postgres/data.sql - after: postgres/reset.sql diff --git a/src/test/kotlin/ltd/hlaeja/controller/DevicesControllerTest.kt b/src/test/kotlin/ltd/hlaeja/controller/DevicesControllerTest.kt new file mode 100644 index 0000000..c66a00d --- /dev/null +++ b/src/test/kotlin/ltd/hlaeja/controller/DevicesControllerTest.kt @@ -0,0 +1,54 @@ +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.DeviceEntity +import ltd.hlaeja.service.DeviceService +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 DevicesControllerTest { + companion object { + const val NIL_UUID: String = "00000000-0000-0000-0000-000000000000" + val id: UUID = UUID.fromString(NIL_UUID) + val type: UUID = UUID.fromString(NIL_UUID) + val timestamp: ZonedDateTime = ZonedDateTime.of(LocalDateTime.of(2000, 1, 1, 0, 0, 0, 1), ZoneId.of("UTC")) + } + + val service: DeviceService = mockk() + + lateinit var controller: DevicesController + + @BeforeEach + fun setUp() { + controller = DevicesController(service) + } + + @Test + fun `get all devices`() = runTest { + // given + coEvery { + service.getDevices(any(), any()) + } returns flowOf(DeviceEntity(id, timestamp, type)) + + // when + val response = controller.getDevices().single() + + // then + coVerify(exactly = 1) { service.getDevices(0, 25) } + + assertThat(response.id).isEqualToUuid(NIL_UUID) + assertThat(response.type).isEqualToUuid(NIL_UUID) + assertThat(response.timestamp).isEqualTo(timestamp) + } +} diff --git a/src/test/kotlin/ltd/hlaeja/controller/TypesControllerTest.kt b/src/test/kotlin/ltd/hlaeja/controller/TypesControllerTest.kt index 1a339e0..425d314 100644 --- a/src/test/kotlin/ltd/hlaeja/controller/TypesControllerTest.kt +++ b/src/test/kotlin/ltd/hlaeja/controller/TypesControllerTest.kt @@ -1,8 +1,8 @@ package ltd.hlaeja.controller -import io.mockk.every +import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.mockk -import io.mockk.verify import java.time.LocalDateTime import java.time.ZoneId import java.time.ZonedDateTime @@ -37,7 +37,7 @@ class TypesControllerTest { @Test fun `get all types`() = runTest { // given - every { + coEvery { service.getTypes(any(), any(), any()) } returns flowOf(TypeEntity(id, timestamp, NAME)) @@ -45,7 +45,7 @@ class TypesControllerTest { val response = controller.getTypes().single() // then - verify(exactly = 1) { service.getTypes(0, 25, null) } + coVerify(exactly = 1) { service.getTypes(0, 25, null) } assertThat(response.id).isEqualToUuid(NIL_UUID) assertThat(response.name).isEqualTo(NAME) diff --git a/src/test/kotlin/ltd/hlaeja/service/TypeServiceTest.kt b/src/test/kotlin/ltd/hlaeja/service/TypeServiceTest.kt index 385dc04..4358fb7 100644 --- a/src/test/kotlin/ltd/hlaeja/service/TypeServiceTest.kt +++ b/src/test/kotlin/ltd/hlaeja/service/TypeServiceTest.kt @@ -6,7 +6,6 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.unmockkStatic -import io.mockk.verify import java.time.LocalDateTime import java.time.ZoneId import java.time.ZonedDateTime @@ -57,29 +56,29 @@ class TypeServiceTest { } @Test - fun `get all types`() { + fun `get all types`() = runTest { // given - every { typeRepository.findAll(any(), any()) } returns flowOf(mockk()) + coEvery { typeRepository.findAll(any(), any()) } returns flowOf(mockk()) // when service.getTypes(1, 10, null) // then - verify(exactly = 1) { typeRepository.findAll(1, 10) } - verify(exactly = 0) { typeRepository.findAllContaining(any(), any(), any()) } + coVerify(exactly = 1) { typeRepository.findAll(1, 10) } + coVerify(exactly = 0) { typeRepository.findAllContaining(any(), any(), any()) } } @Test - fun `get all types with filter`() { + fun `get all types with filter`() = runTest { // given - every { typeRepository.findAllContaining(any(), any(), any()) } returns flowOf(mockk()) + coEvery { typeRepository.findAllContaining(any(), any(), any()) } returns flowOf(mockk()) // when service.getTypes(1, 10, "abc") // then - verify(exactly = 1) { typeRepository.findAllContaining("%abc%", 1, 10) } - verify(exactly = 0) { typeRepository.findAll(any(), any()) } + coVerify(exactly = 1) { typeRepository.findAllContaining("%abc%", 1, 10) } + coVerify(exactly = 0) { typeRepository.findAll(any(), any()) } } @Test