From 0c4a5d0af6bcf0d99c48d60e485b7cbf1845805c Mon Sep 17 00:00:00 2001 From: Swordsteel Date: Tue, 11 Mar 2025 07:51:40 +0100 Subject: [PATCH] add get and update for Type - add UpdateType end-to-end - add updateType to TypeController - add updateType to TypeService - add sql 004-create_type_description_data.sql - update TypesEndpoint to use Types.Response - update type end-to-end test - update TypeEndpoint with CreateType - add reset test table - add test data - add getType to TypeController - add getType to TypeService - add findTypeWithDescription to TypeRepository - update type end-to-end test - update TypeController for changes for adding type - update type mapping for latest changes in Mapping.kt - update addType to use TypeDescriptionRepository and return TypeWithDescription in TypeService - add TypeWithDescription - add TypeDescriptionRepository - add TypeDescriptionEntity - add missing device mapping test - add type_descriptions sql script for database changes - update TypesEndpoint - update TypesController to use Types.Response - add TypeEntity.toTypesResponse to Mapping.kt --- http/type.http | 17 +- sql/003-type_descriptions.sql | 24 ++ sql/004-create_type_description_data.sql | 5 + sql/initial/005-type_descriptions.sql | 24 ++ .../ltd/hlaeja/controller/TypeEndpoint.kt | 220 +++++++++++++- .../ltd/hlaeja/controller/TypesEndpoint.kt | 14 +- .../resources/postgres/data.sql | 6 + .../resources/postgres/reset.sql | 1 + .../resources/postgres/schema.sql | 14 + .../ltd/hlaeja/controller/TypeController.kt | 23 +- .../ltd/hlaeja/controller/TypesController.kt | 8 +- .../ltd/hlaeja/dto/TypeWithDescription.kt | 11 + .../hlaeja/entity/TypeDescriptionEntity.kt | 12 + .../repository/TypeDescriptionRepository.kt | 24 ++ .../ltd/hlaeja/repository/TypeRepository.kt | 14 +- .../kotlin/ltd/hlaeja/service/TypeService.kt | 102 ++++++- src/main/kotlin/ltd/hlaeja/util/Mapping.kt | 29 +- .../hlaeja/controller/TypeControllerTest.kt | 51 +++- .../hlaeja/controller/TypesControllerTest.kt | 15 +- .../ltd/hlaeja/service/TypeServiceTest.kt | 283 ++++++++++++++++-- .../kotlin/ltd/hlaeja/util/MappingKtTest.kt | 134 +++++++-- 21 files changed, 947 insertions(+), 84 deletions(-) create mode 100644 sql/003-type_descriptions.sql create mode 100644 sql/004-create_type_description_data.sql create mode 100644 sql/initial/005-type_descriptions.sql create mode 100644 src/main/kotlin/ltd/hlaeja/dto/TypeWithDescription.kt create mode 100644 src/main/kotlin/ltd/hlaeja/entity/TypeDescriptionEntity.kt create mode 100644 src/main/kotlin/ltd/hlaeja/repository/TypeDescriptionRepository.kt diff --git a/http/type.http b/http/type.http index 532bd1c..4b92960 100644 --- a/http/type.http +++ b/http/type.http @@ -1,7 +1,20 @@ -### add type by name +### add type POST {{hostname}}/type Content-Type: application/json { - "name": "Test C" + "name": "Test Device 001", + "description": "Description of test device." +} + +### get type by id +GET {{hostname}}/type-00000000-0000-0000-0000-000000000000 + +### update type by id +PUT {{hostname}}/type-00000000-0000-0000-0000-000000000000 +Content-Type: application/json + +{ + "name": "Test Device 001", + "description": "Description of test device." } diff --git a/sql/003-type_descriptions.sql b/sql/003-type_descriptions.sql new file mode 100644 index 0000000..7dfb768 --- /dev/null +++ b/sql/003-type_descriptions.sql @@ -0,0 +1,24 @@ +-- Table: public.type_descriptions +-- DROP TABLE IF EXISTS public.type_descriptions; + +CREATE TABLE IF NOT EXISTS public.type_descriptions +( + type_id uuid NOT NULL, + description character varying(1000) COLLATE pg_catalog."default" NOT NULL DEFAULT ''::character varying, + CONSTRAINT pk_type_descriptions PRIMARY KEY (type_id), + CONSTRAINT fk_type_descriptions_types FOREIGN KEY (type_id) + REFERENCES public.types (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE +); + +ALTER TABLE IF EXISTS public.type_descriptions + OWNER to role_administrator; + +-- Revoke all permissions from existing roles +REVOKE ALL ON TABLE public.type_descriptions FROM role_administrator, role_maintainer, role_support, role_service; + +-- Grant appropriate permissions +GRANT ALL ON TABLE public.type_descriptions TO role_administrator; +GRANT SELECT, INSERT, UPDATE ON TABLE public.type_descriptions TO role_maintainer, role_service; +GRANT SELECT ON TABLE public.type_descriptions TO role_support; diff --git a/sql/004-create_type_description_data.sql b/sql/004-create_type_description_data.sql new file mode 100644 index 0000000..933879b --- /dev/null +++ b/sql/004-create_type_description_data.sql @@ -0,0 +1,5 @@ +-- make type description for existing types +INSERT INTO public.type_descriptions (type_id) +SELECT id +FROM public.types +ON CONFLICT (type_id) DO NOTHING; diff --git a/sql/initial/005-type_descriptions.sql b/sql/initial/005-type_descriptions.sql new file mode 100644 index 0000000..7dfb768 --- /dev/null +++ b/sql/initial/005-type_descriptions.sql @@ -0,0 +1,24 @@ +-- Table: public.type_descriptions +-- DROP TABLE IF EXISTS public.type_descriptions; + +CREATE TABLE IF NOT EXISTS public.type_descriptions +( + type_id uuid NOT NULL, + description character varying(1000) COLLATE pg_catalog."default" NOT NULL DEFAULT ''::character varying, + CONSTRAINT pk_type_descriptions PRIMARY KEY (type_id), + CONSTRAINT fk_type_descriptions_types FOREIGN KEY (type_id) + REFERENCES public.types (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE +); + +ALTER TABLE IF EXISTS public.type_descriptions + OWNER to role_administrator; + +-- Revoke all permissions from existing roles +REVOKE ALL ON TABLE public.type_descriptions FROM role_administrator, role_maintainer, role_support, role_service; + +-- Grant appropriate permissions +GRANT ALL ON TABLE public.type_descriptions TO role_administrator; +GRANT SELECT, INSERT, UPDATE ON TABLE public.type_descriptions TO role_maintainer, role_service; +GRANT SELECT ON TABLE public.type_descriptions TO role_support; diff --git a/src/integration-test/kotlin/ltd/hlaeja/controller/TypeEndpoint.kt b/src/integration-test/kotlin/ltd/hlaeja/controller/TypeEndpoint.kt index affab1b..6b5aa2b 100644 --- a/src/integration-test/kotlin/ltd/hlaeja/controller/TypeEndpoint.kt +++ b/src/integration-test/kotlin/ltd/hlaeja/controller/TypeEndpoint.kt @@ -2,6 +2,7 @@ package ltd.hlaeja.controller import ltd.hlaeja.library.deviceRegistry.Type import ltd.hlaeja.test.container.PostgresContainer +import ltd.hlaeja.test.isEqualToUuid import org.assertj.core.api.SoftAssertions import org.assertj.core.api.junit.jupiter.InjectSoftAssertions import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension @@ -9,10 +10,14 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested 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.http.HttpStatus.CONFLICT +import org.springframework.http.HttpStatus.NO_CONTENT +import org.springframework.http.MediaType.APPLICATION_JSON import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.test.web.reactive.server.expectBody @@ -40,9 +45,11 @@ class TypeEndpoint { @Test fun `added type - success`() { // given - val name = "Thing 5" + val name = "Thing 5 v1" + val description = "Thing 5 description" val request = Type.Request( name = name, + description = description, ) // when @@ -50,11 +57,12 @@ class TypeEndpoint { // then result.expectStatus() - .isOk + .isCreated .expectBody() .consumeWith { softly.assertThat(it.responseBody?.id?.version()).isEqualTo(7) softly.assertThat(it.responseBody?.name).isEqualTo(name) + softly.assertThat(it.responseBody?.description).isEqualTo(description) } } @@ -63,6 +71,7 @@ class TypeEndpoint { // given val request = Type.Request( name = "Thing 1 v1", + description = "Thing 1 description", ) // when @@ -71,5 +80,212 @@ class TypeEndpoint { // then result.expectStatus().isEqualTo(CONFLICT) } + + @ParameterizedTest + @CsvSource( + value = [ + "{}", + "{'name': 'Thing 0 v1'}", + "{'description': 'Thing 0 description'}", + ], + ) + fun `added type - fail bad request`(jsonRequest: String) { + // when + val result = webClient.post() + .uri("/type") + .contentType(APPLICATION_JSON) // Set Content-Type header + .bodyValue(jsonRequest) // Send raw JSON string + .exchange() + + // then + result.expectStatus().isBadRequest + } + } + + @Nested + inner class GetType { + + @Test + fun `added type - success`() { + // when + val result = webClient.get().uri("/type-00000000-0000-0000-0001-000000000001").exchange() + + // then + result.expectStatus() + .isOk + .expectBody() + .consumeWith { + softly.assertThat(it.responseBody?.id).isEqualToUuid("00000000-0000-0000-0001-000000000001") + softly.assertThat(it.responseBody?.name).isEqualTo("Thing 1 v1") + softly.assertThat(it.responseBody?.description).isEqualTo("Thing 1 description") + } + } + + @Test + fun `get type - fail not found`() { + // when + val result = webClient.get().uri("/type-00000000-0000-0000-0000-000000000000").exchange() + + // then + result.expectStatus().isNotFound + } + + @ParameterizedTest + @CsvSource( + value = [ + "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz", + "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", + "00000000000000000000000000000000", + "0", + ], + ) + fun `get type - fail bad request`(uuid: String) { + // when + val result = webClient.get().uri("/type-$uuid").exchange() + + // then + result.expectStatus().isBadRequest + } + } + + @Nested + inner class UpdateType { + + @ParameterizedTest + @CsvSource( + value = [ + "Thing 4 v1,Thing 4 description update", + "Thing 4 v1 update,Thing 4 description update", + "Thing 4 v1,Thing 4 description", + ], + ) + fun `update type - success`(name: String, description: String) { + // given + val request = Type.Request( + name = name, + description = description, + ) + + // when + val result = webClient.put() + .uri("/type-00000000-0000-0000-0001-000000000004") + .contentType(APPLICATION_JSON) + .bodyValue(request) + .exchange() + + // then + result.expectStatus() + .isOk + .expectBody() + .consumeWith { + softly.assertThat(it.responseBody?.id).isEqualToUuid("00000000-0000-0000-0001-000000000004") + softly.assertThat(it.responseBody?.name).isEqualTo(name) + softly.assertThat(it.responseBody?.description).isEqualTo(description) + } + } + + @Test + fun `update type - success no change`() { + // given + val request = Type.Request( + name = "Thing 1 v1", + description = "Thing 1 description", + ) + + // when + val result = webClient.put() + .uri("/type-00000000-0000-0000-0001-000000000001") + .contentType(APPLICATION_JSON) + .bodyValue(request) + .exchange() + + // then + result.expectStatus().isEqualTo(NO_CONTENT) + } + + @Test + fun `update type - fail invalid id`() { + // given + val request = Type.Request( + name = "Thing 0 v1", + description = "Thing 0 description", + ) + + // when + val result = webClient.put() + .uri("/type-00000000-0000-0000-0001-000000000000") + .contentType(APPLICATION_JSON) + .bodyValue(request) + .exchange() + + // then + result.expectStatus().isNotFound + } + + @Test + fun `update type - fail name take`() { + // given + val request = Type.Request( + name = "Thing 2 v1", + description = "Thing 2 description", + ) + + // when + val result = webClient.put() + .uri("/type-00000000-0000-0000-0001-000000000001") + .contentType(APPLICATION_JSON) + .bodyValue(request) + .exchange() + + // then + result.expectStatus().isEqualTo(CONFLICT) + } + + @ParameterizedTest + @CsvSource( + value = [ + "{}", + "{'name': 'Thing 0 v1'}", + "{'description': 'Thing 0 description'}", + ], + ) + fun `update type - fail bad data request`(jsonRequest: String) { + // when + val result = webClient.put() + .uri("/type-00000000-0000-0000-0001-000000000001") + .contentType(APPLICATION_JSON) + .bodyValue(jsonRequest) + .exchange() + + // then + result.expectStatus().isBadRequest + } + + @ParameterizedTest + @CsvSource( + value = [ + "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz", + "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", + "00000000000000000000000000000000", + "0", + ], + ) + fun `update type - fail bad id request`(uuid: String) { + // given + val request = Type.Request( + name = "Thing 0 v1", + description = "Thing 0 description", + ) + + // when + val result = webClient.put() + .uri("/type-$uuid") + .contentType(APPLICATION_JSON) + .bodyValue(request) + .exchange() + + // then + result.expectStatus().isBadRequest + } } } diff --git a/src/integration-test/kotlin/ltd/hlaeja/controller/TypesEndpoint.kt b/src/integration-test/kotlin/ltd/hlaeja/controller/TypesEndpoint.kt index 1fc5e0b..064fb47 100644 --- a/src/integration-test/kotlin/ltd/hlaeja/controller/TypesEndpoint.kt +++ b/src/integration-test/kotlin/ltd/hlaeja/controller/TypesEndpoint.kt @@ -1,6 +1,6 @@ package ltd.hlaeja.controller -import ltd.hlaeja.library.deviceRegistry.Type +import ltd.hlaeja.library.deviceRegistry.Types import ltd.hlaeja.test.container.PostgresContainer import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach @@ -36,7 +36,7 @@ class TypesEndpoint { // then result.expectStatus().isOk() - .expectBody>() + .expectBody>() .consumeWith { assertThat(it.responseBody?.size).isEqualTo(4) } @@ -55,7 +55,7 @@ class TypesEndpoint { // then result.expectStatus().isOk() - .expectBody>() + .expectBody>() .consumeWith { assertThat(it.responseBody?.size).isEqualTo(expected) } @@ -84,7 +84,7 @@ class TypesEndpoint { // then result.expectStatus().isOk() - .expectBody>() + .expectBody>() .consumeWith { assertThat(it.responseBody?.size).isEqualTo(expected) } @@ -120,7 +120,7 @@ class TypesEndpoint { // then result.expectStatus().isOk() - .expectBody>() + .expectBody>() .consumeWith { assertThat(it.responseBody?.size).isEqualTo(expected) } @@ -144,7 +144,7 @@ class TypesEndpoint { // then result.expectStatus().isOk() - .expectBody>() + .expectBody>() .consumeWith { assertThat(it.responseBody?.size).isEqualTo(expected) } @@ -185,7 +185,7 @@ class TypesEndpoint { // then result.expectStatus().isOk() - .expectBody>() + .expectBody>() .consumeWith { assertThat(it.responseBody?.size).isEqualTo(expected) } diff --git a/src/integration-test/resources/postgres/data.sql b/src/integration-test/resources/postgres/data.sql index 4ec5c92..62b8fac 100644 --- a/src/integration-test/resources/postgres/data.sql +++ b/src/integration-test/resources/postgres/data.sql @@ -5,6 +5,12 @@ VALUES ('00000000-0000-0000-0001-000000000001'::uuid, '2000-01-01 00:00:00.00000 ('00000000-0000-0000-0001-000000000003'::uuid, '2000-01-01 00:00:00.000001 +00:00', 'Thing 3 v2'), ('00000000-0000-0000-0001-000000000004'::uuid, '2000-01-01 00:00:00.000001 +00:00', 'Thing 4 v1'); +INSERT INTO public.type_descriptions (type_id, description) +VALUES ('00000000-0000-0000-0001-000000000001'::uuid, 'Thing 1 description'), + ('00000000-0000-0000-0001-000000000002'::uuid, 'Thing 2 description'), + ('00000000-0000-0000-0001-000000000003'::uuid, 'Thing 3 description'), + ('00000000-0000-0000-0001-000000000004'::uuid, 'Thing 4 description'); + INSERT INTO public.devices (id, timestamp, type) VALUES ('00000000-0000-0000-0002-000000000001'::uuid, '2000-01-01 00:00:00.000001 +00:00', '00000000-0000-0000-0001-000000000001'::uuid), ('00000000-0000-0000-0002-000000000002'::uuid, '2000-01-01 00:00:00.000001 +00:00', '00000000-0000-0000-0001-000000000001'::uuid), diff --git a/src/integration-test/resources/postgres/reset.sql b/src/integration-test/resources/postgres/reset.sql index 919c38c..442d2c9 100644 --- a/src/integration-test/resources/postgres/reset.sql +++ b/src/integration-test/resources/postgres/reset.sql @@ -3,6 +3,7 @@ -- Truncate tables TRUNCATE TABLE nodes CASCADE; TRUNCATE TABLE devices CASCADE; +TRUNCATE TABLE type_descriptions CASCADE; TRUNCATE TABLE types CASCADE; -- Enable triggers on the account table diff --git a/src/integration-test/resources/postgres/schema.sql b/src/integration-test/resources/postgres/schema.sql index f83239a..736c86f 100644 --- a/src/integration-test/resources/postgres/schema.sql +++ b/src/integration-test/resources/postgres/schema.sql @@ -31,6 +31,20 @@ CREATE TABLE IF NOT EXISTS public.types CONSTRAINT pk_contact_types PRIMARY KEY (id) ); +CREATE UNIQUE INDEX IF NOT EXISTS types_name_key + ON types (name ASC); + +CREATE TABLE IF NOT EXISTS public.type_descriptions +( + type_id uuid NOT NULL, + description character varying(1000) COLLATE pg_catalog."default" NOT NULL DEFAULT ''::character varying, + CONSTRAINT pk_type_descriptions PRIMARY KEY (type_id), + CONSTRAINT fk_type_descriptions_types FOREIGN KEY (type_id) + REFERENCES public.types (id) MATCH SIMPLE + ON UPDATE NO ACTION + ON DELETE CASCADE +); + -- Table: public.devices CREATE TABLE IF NOT EXISTS public.devices diff --git a/src/main/kotlin/ltd/hlaeja/controller/TypeController.kt b/src/main/kotlin/ltd/hlaeja/controller/TypeController.kt index dd93c8b..8a71f6f 100644 --- a/src/main/kotlin/ltd/hlaeja/controller/TypeController.kt +++ b/src/main/kotlin/ltd/hlaeja/controller/TypeController.kt @@ -1,11 +1,16 @@ package ltd.hlaeja.controller +import java.util.UUID import ltd.hlaeja.library.deviceRegistry.Type import ltd.hlaeja.service.TypeService -import ltd.hlaeja.util.toTypeEntity import ltd.hlaeja.util.toTypeResponse +import org.springframework.http.HttpStatus.CREATED +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.PutMapping import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController @RestController @@ -14,7 +19,19 @@ class TypeController( ) { @PostMapping("/type") + @ResponseStatus(CREATED) suspend fun addType( - @RequestBody register: Type.Request, - ): Type.Response = service.addType(register.toTypeEntity()).toTypeResponse() + @RequestBody request: Type.Request, + ): Type.Response = service.addType(request.name, request.description).toTypeResponse() + + @GetMapping("/type-{type}") + suspend fun getType( + @PathVariable type: UUID, + ): Type.Response = service.getType(type).toTypeResponse() + + @PutMapping("/type-{type}") + suspend fun updateType( + @PathVariable type: UUID, + @RequestBody request: Type.Request, + ): Type.Response = service.updateType(type, request.name, request.description).toTypeResponse() } diff --git a/src/main/kotlin/ltd/hlaeja/controller/TypesController.kt b/src/main/kotlin/ltd/hlaeja/controller/TypesController.kt index f591326..65fb8c9 100644 --- a/src/main/kotlin/ltd/hlaeja/controller/TypesController.kt +++ b/src/main/kotlin/ltd/hlaeja/controller/TypesController.kt @@ -3,9 +3,9 @@ package ltd.hlaeja.controller import jakarta.validation.constraints.Min import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import ltd.hlaeja.library.deviceRegistry.Type +import ltd.hlaeja.library.deviceRegistry.Types import ltd.hlaeja.service.TypeService -import ltd.hlaeja.util.toTypeResponse +import ltd.hlaeja.util.toTypesResponse import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RestController @@ -31,6 +31,6 @@ class TypesController( @PathVariable(required = false) @Min(1) page: Int = DEFAULT_PAGE, @PathVariable(required = false) @Min(1) show: Int = DEFAULT_SIZE, @PathVariable(required = false) filter: String? = null, - ): Flow = service.getTypes((page - 1) * show, show, filter) - .map { it.toTypeResponse() } + ): Flow = service.getTypes((page - 1) * show, show, filter) + .map { it.toTypesResponse() } } diff --git a/src/main/kotlin/ltd/hlaeja/dto/TypeWithDescription.kt b/src/main/kotlin/ltd/hlaeja/dto/TypeWithDescription.kt new file mode 100644 index 0000000..c4bcb2c --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/dto/TypeWithDescription.kt @@ -0,0 +1,11 @@ +package ltd.hlaeja.dto + +import java.time.ZonedDateTime +import java.util.UUID + +data class TypeWithDescription( + val id: UUID, + val timestamp: ZonedDateTime, + val name: String, + val description: String?, +) diff --git a/src/main/kotlin/ltd/hlaeja/entity/TypeDescriptionEntity.kt b/src/main/kotlin/ltd/hlaeja/entity/TypeDescriptionEntity.kt new file mode 100644 index 0000000..b228230 --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/entity/TypeDescriptionEntity.kt @@ -0,0 +1,12 @@ +package ltd.hlaeja.entity + +import java.util.UUID +import org.springframework.data.annotation.Id +import org.springframework.data.relational.core.mapping.Table + +@Table("type_descriptions") +data class TypeDescriptionEntity( + @Id + val typeId: UUID, + val description: String, +) diff --git a/src/main/kotlin/ltd/hlaeja/repository/TypeDescriptionRepository.kt b/src/main/kotlin/ltd/hlaeja/repository/TypeDescriptionRepository.kt new file mode 100644 index 0000000..2cc734b --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/repository/TypeDescriptionRepository.kt @@ -0,0 +1,24 @@ +package ltd.hlaeja.repository + +import java.util.UUID +import ltd.hlaeja.entity.TypeDescriptionEntity +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 TypeDescriptionRepository : CoroutineCrudRepository { + @Query( + """ + INSERT INTO type_descriptions (type_id, description) VALUES (:type_id, :description) + ON CONFLICT (type_id) + DO UPDATE SET description = :description + RETURNING * + """, + ) + suspend fun upsert( + @Param("type_id") typeId: UUID, + @Param("description") description: String, + ): TypeDescriptionEntity +} diff --git a/src/main/kotlin/ltd/hlaeja/repository/TypeRepository.kt b/src/main/kotlin/ltd/hlaeja/repository/TypeRepository.kt index 71b9a18..541e868 100644 --- a/src/main/kotlin/ltd/hlaeja/repository/TypeRepository.kt +++ b/src/main/kotlin/ltd/hlaeja/repository/TypeRepository.kt @@ -2,8 +2,8 @@ package ltd.hlaeja.repository import java.util.UUID import kotlinx.coroutines.flow.Flow +import ltd.hlaeja.dto.TypeWithDescription import ltd.hlaeja.entity.TypeEntity - import org.springframework.data.r2dbc.repository.Query import org.springframework.data.repository.kotlin.CoroutineCrudRepository import org.springframework.data.repository.query.Param @@ -24,4 +24,16 @@ interface TypeRepository : CoroutineCrudRepository { @Param("offset") offset: Int, @Param("limit") limit: Int, ): Flow + + @Query( + """ + SELECT t.id, t.timestamp, t.name, td.description + FROM types t + LEFT JOIN type_descriptions td ON t.id = td.type_id + WHERE t.id = :id + """, + ) + suspend fun findTypeWithDescription( + @Param("id") id: UUID, + ): TypeWithDescription? } diff --git a/src/main/kotlin/ltd/hlaeja/service/TypeService.kt b/src/main/kotlin/ltd/hlaeja/service/TypeService.kt index 8334c4f..6eb05bb 100644 --- a/src/main/kotlin/ltd/hlaeja/service/TypeService.kt +++ b/src/main/kotlin/ltd/hlaeja/service/TypeService.kt @@ -1,12 +1,21 @@ 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.dto.TypeWithDescription +import ltd.hlaeja.entity.TypeDescriptionEntity import ltd.hlaeja.entity.TypeEntity +import ltd.hlaeja.repository.TypeDescriptionRepository import ltd.hlaeja.repository.TypeRepository -import org.springframework.dao.DuplicateKeyException -import org.springframework.http.HttpStatus +import org.springframework.dao.DataIntegrityViolationException +import org.springframework.http.HttpStatus.CONFLICT +import org.springframework.http.HttpStatus.EXPECTATION_FAILED +import org.springframework.http.HttpStatus.NOT_FOUND +import org.springframework.http.HttpStatus.NO_CONTENT import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional import org.springframework.web.server.ResponseStatusException private val log = KotlinLogging.logger {} @@ -14,6 +23,7 @@ private val log = KotlinLogging.logger {} @Service class TypeService( private val typeRepository: TypeRepository, + private val typeDescriptionRepository: TypeDescriptionRepository, ) { fun getTypes( @@ -25,13 +35,87 @@ class TypeService( else -> typeRepository.findAll(page, show) } + @Transactional suspend fun addType( - entity: TypeEntity, - ): TypeEntity = try { - typeRepository.save(entity) - .also { log.debug { "Added new type: $it.id" } } - } catch (e: DuplicateKeyException) { - log.warn { e.localizedMessage } - throw ResponseStatusException(HttpStatus.CONFLICT) + name: String, + description: String, + ): TypeWithDescription = try { + val savedType = typeRepository.save( + TypeEntity(timestamp = ZonedDateTime.now(), name = name), + ).also { log.debug { "Added new type: ${it.id}" } } + val savedDescription = typeDescriptionRepository.upsert( + savedType.id ?: throw ResponseStatusException(EXPECTATION_FAILED), + description, + ).also { log.debug { "Added description for type: ${it.typeId}" } } + TypeWithDescription( + id = savedType.id, + timestamp = savedType.timestamp, + name = savedType.name, + description = savedDescription.description, + ) + } catch (e: DataIntegrityViolationException) { + log.warn { "Failed to add type with name '$name': ${e.localizedMessage}" } + throw ResponseStatusException(CONFLICT, "Type with name '$name' already exists") + } + + suspend fun getType( + id: UUID, + ): TypeWithDescription = typeRepository.findTypeWithDescription(id) + ?.also { log.debug { "Retrieved type with description: ${it.id}" } } + ?: throw ResponseStatusException(NOT_FOUND, "Type with id '$id' not found") + + @Transactional + suspend fun updateType( + id: UUID, + name: String, + description: String, + ): TypeWithDescription { + var hasChanges = false + val updatedType = updateType(id, name) { hasChanges = true } + val updatedTypeDescription = updateTypeDescription(id, description) { hasChanges = true } + if (!hasChanges) { + throw ResponseStatusException(NO_CONTENT, "No changes for type with id '$id'") + } + return TypeWithDescription( + id = updatedType.id!!, + timestamp = updatedType.timestamp, + name = updatedType.name, + description = updatedTypeDescription.description, + ) + } + + private suspend fun updateTypeDescription( + id: UUID, + description: String, + onChange: () -> Unit, + ): TypeDescriptionEntity { + val existingDescription = typeDescriptionRepository.findById(id) + ?: throw ResponseStatusException(NOT_FOUND, "Type description with id '$id' not found") + return if (existingDescription.description == description) { + existingDescription + } else { + onChange() + typeDescriptionRepository.save(existingDescription.copy(description = description)) + } + } + + private suspend fun updateType( + id: UUID, + name: String, + onChange: () -> Unit, + ): TypeEntity { + val existingType = typeRepository.findById(id) + ?: throw ResponseStatusException(NOT_FOUND, "Type with id '$id' not found") + return if (existingType.name == name) { + existingType + } else { + onChange() + try { + typeRepository.save(existingType.copy(name = name)) + } catch (e: DataIntegrityViolationException) { + log.warn { "Failed to update type with name '$name': ${e.localizedMessage}" } + throw ResponseStatusException(CONFLICT, "Type with name '$name' already exists") + } + } } } diff --git a/src/main/kotlin/ltd/hlaeja/util/Mapping.kt b/src/main/kotlin/ltd/hlaeja/util/Mapping.kt index 1503e2e..508855b 100644 --- a/src/main/kotlin/ltd/hlaeja/util/Mapping.kt +++ b/src/main/kotlin/ltd/hlaeja/util/Mapping.kt @@ -1,22 +1,43 @@ package ltd.hlaeja.util import java.time.ZonedDateTime +import java.util.UUID 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.jwt.service.PrivateJwtService import ltd.hlaeja.library.deviceRegistry.Device import ltd.hlaeja.library.deviceRegistry.Identity import ltd.hlaeja.library.deviceRegistry.Node import ltd.hlaeja.library.deviceRegistry.Type +import ltd.hlaeja.library.deviceRegistry.Types import org.springframework.http.HttpStatus.EXPECTATION_FAILED import org.springframework.web.server.ResponseStatusException -fun Type.Request.toTypeEntity(): TypeEntity = TypeEntity(null, ZonedDateTime.now(), name) +fun Type.Request.toTypeEntity(id: UUID): TypeEntity = TypeEntity( + id = id, + timestamp = ZonedDateTime.now(), + name = name, +) -fun TypeEntity.toTypeResponse(): Type.Response = Type.Response( - id ?: throw ResponseStatusException(EXPECTATION_FAILED), - name, +fun Type.Request.toTypeDescriptionEntity(id: UUID): TypeDescriptionEntity = TypeDescriptionEntity( + typeId = id, + description = description, +) + +fun TypeWithDescription.toTypeResponse(): Type.Response = Type.Response( + id = id, + timestamp = timestamp, + name = name, + description = description ?: "", +) + +fun TypeEntity.toTypesResponse(): Types.Response = Types.Response( + id = id!!, + name = name, + timestamp = timestamp, ) fun Node.Request.toEntity(): NodeEntity = NodeEntity( diff --git a/src/test/kotlin/ltd/hlaeja/controller/TypeControllerTest.kt b/src/test/kotlin/ltd/hlaeja/controller/TypeControllerTest.kt index 2355a68..c996afd 100644 --- a/src/test/kotlin/ltd/hlaeja/controller/TypeControllerTest.kt +++ b/src/test/kotlin/ltd/hlaeja/controller/TypeControllerTest.kt @@ -8,7 +8,7 @@ import java.time.ZoneId import java.time.ZonedDateTime import java.util.UUID import kotlinx.coroutines.test.runTest -import ltd.hlaeja.entity.TypeEntity +import ltd.hlaeja.dto.TypeWithDescription import ltd.hlaeja.library.deviceRegistry.Type import ltd.hlaeja.service.TypeService import ltd.hlaeja.test.isEqualToUuid @@ -34,16 +34,59 @@ class TypeControllerTest { @Test fun `add type`() = runTest { // given - val request = Type.Request("name") - coEvery { service.addType(any()) } returns TypeEntity(id, timestamp, "name") + val request = Type.Request("name", "description") + coEvery { service.addType(any(), any()) } returns TypeWithDescription(id, timestamp, "name", "description") // when val response = controller.addType(request) // then - coVerify(exactly = 1) { service.addType(any()) } + coVerify(exactly = 1) { service.addType(any(), any()) } assertThat(response.id).isEqualToUuid("00000000-0000-0000-0000-000000000000") assertThat(response.name).isEqualTo("name") + assertThat(response.description).isEqualTo("description") + } + + @Test + fun `get type`() = runTest { + // given + coEvery { service.getType(any()) } returns TypeWithDescription(id, timestamp, "name", "description") + + // when + val response = controller.getType(id) + + // then + coVerify(exactly = 1) { service.getType(any()) } + + assertThat(response.id).isEqualToUuid("00000000-0000-0000-0000-000000000000") + assertThat(response.name).isEqualTo("name") + assertThat(response.description).isEqualTo("description") + } + + @Test + fun `update type`() = runTest { + // given + val request = Type.Request("name", "description") + + coEvery { service.updateType(any(), any(), any()) } answers { call -> + TypeWithDescription( + id = call.invocation.args[0] as UUID, + timestamp = timestamp, + name = call.invocation.args[1] as String, + description = call.invocation.args[2] as String, + ) + } + + // when + val response = controller.updateType(id, request) + + // then + coVerify(exactly = 1) { service.updateType(any(), any(), any()) } + + assertThat(response.id).isEqualToUuid("00000000-0000-0000-0000-000000000000") + assertThat(response.timestamp).isEqualTo(timestamp) + assertThat(response.name).isEqualTo("name") + assertThat(response.description).isEqualTo("description") } } diff --git a/src/test/kotlin/ltd/hlaeja/controller/TypesControllerTest.kt b/src/test/kotlin/ltd/hlaeja/controller/TypesControllerTest.kt index 874d1a3..1a339e0 100644 --- a/src/test/kotlin/ltd/hlaeja/controller/TypesControllerTest.kt +++ b/src/test/kotlin/ltd/hlaeja/controller/TypesControllerTest.kt @@ -19,8 +19,10 @@ import org.junit.jupiter.api.Test class TypesControllerTest { companion object { - val id = UUID.fromString("00000000-0000-0000-0000-000000000000") - val timestamp = ZonedDateTime.of(LocalDateTime.of(2000, 1, 1, 0, 0, 0, 1), ZoneId.of("UTC")) + const val NAME: String = "name" + const val NIL_UUID: String = "00000000-0000-0000-0000-000000000000" + val id: UUID = UUID.fromString(NIL_UUID) + val timestamp: ZonedDateTime = ZonedDateTime.of(LocalDateTime.of(2000, 1, 1, 0, 0, 0, 1), ZoneId.of("UTC")) } val service: TypeService = mockk() @@ -35,7 +37,9 @@ class TypesControllerTest { @Test fun `get all types`() = runTest { // given - every { service.getTypes(any(), any(), any()) } returns flowOf(TypeEntity(id, timestamp, "name")) + every { + service.getTypes(any(), any(), any()) + } returns flowOf(TypeEntity(id, timestamp, NAME)) // when val response = controller.getTypes().single() @@ -43,7 +47,8 @@ class TypesControllerTest { // then verify(exactly = 1) { service.getTypes(0, 25, null) } - assertThat(response.id).isEqualToUuid("00000000-0000-0000-0000-000000000000") - assertThat(response.name).isEqualTo("name") + assertThat(response.id).isEqualToUuid(NIL_UUID) + assertThat(response.name).isEqualTo(NAME) + assertThat(response.timestamp).isEqualTo(timestamp) } } diff --git a/src/test/kotlin/ltd/hlaeja/service/TypeServiceTest.kt b/src/test/kotlin/ltd/hlaeja/service/TypeServiceTest.kt index 2641c17..450e9b3 100644 --- a/src/test/kotlin/ltd/hlaeja/service/TypeServiceTest.kt +++ b/src/test/kotlin/ltd/hlaeja/service/TypeServiceTest.kt @@ -4,90 +4,321 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkStatic import io.mockk.verify -import java.time.Instant +import java.time.LocalDateTime import java.time.ZoneId import java.time.ZonedDateTime import java.util.UUID import kotlin.test.assertFailsWith import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest +import ltd.hlaeja.dto.TypeWithDescription +import ltd.hlaeja.entity.TypeDescriptionEntity import ltd.hlaeja.entity.TypeEntity +import ltd.hlaeja.repository.TypeDescriptionRepository import ltd.hlaeja.repository.TypeRepository +import ltd.hlaeja.test.isEqualToUuid +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.dao.DuplicateKeyException +import org.springframework.http.HttpStatus.CONFLICT +import org.springframework.http.HttpStatus.EXPECTATION_FAILED +import org.springframework.http.HttpStatus.NOT_FOUND +import org.springframework.http.HttpStatus.NO_CONTENT import org.springframework.web.server.ResponseStatusException class TypeServiceTest { + companion object { - val timestamp = ZonedDateTime.ofInstant(Instant.parse("2000-01-01T00:00:00.001Z"), ZoneId.of("UTC")) + val timestamp: ZonedDateTime = ZonedDateTime.of(LocalDateTime.of(2000, 1, 1, 0, 0, 0, 1), ZoneId.of("UTC")) val uuid = UUID.fromString("00000000-0000-0000-0000-000000000000") } - val repository: TypeRepository = mockk() + val typeRepository: TypeRepository = mockk() + val typeDescriptionRepository: TypeDescriptionRepository = mockk() lateinit var service: TypeService @BeforeEach fun setUp() { - service = TypeService(repository) + service = TypeService(typeRepository, typeDescriptionRepository) + + mockkStatic(ZonedDateTime::class) + every { ZonedDateTime.now() } returns timestamp + } + + @AfterEach + fun tearDown() { + unmockkStatic(ZonedDateTime::class) } @Test fun `get all types`() { // given - every { repository.findAll(any(), any()) } returns flowOf(mockk()) + every { typeRepository.findAll(any(), any()) } returns flowOf(mockk()) // when service.getTypes(1, 10, null) // then - verify(exactly = 1) { repository.findAll(1, 10) } - verify(exactly = 0) { repository.findAllContaining(any(), any(), any()) } + verify(exactly = 1) { typeRepository.findAll(1, 10) } + verify(exactly = 0) { typeRepository.findAllContaining(any(), any(), any()) } } @Test fun `get all types with filter`() { // given - every { repository.findAllContaining(any(), any(), any()) } returns flowOf(mockk()) + every { typeRepository.findAllContaining(any(), any(), any()) } returns flowOf(mockk()) // when service.getTypes(1, 10, "abc") // then - verify(exactly = 1) { repository.findAllContaining("%abc%", 1, 10) } - verify(exactly = 0) { repository.findAll(any(), any()) } + verify(exactly = 1) { typeRepository.findAllContaining("%abc%", 1, 10) } + verify(exactly = 0) { typeRepository.findAll(any(), any()) } } @Test fun `add new type success`() = runTest { // given - val entity = TypeEntity( - null, - timestamp, - "name", - ) - - coEvery { repository.save(any()) } answers { call -> (call.invocation.args[0] as TypeEntity).copy(id = uuid) } + coEvery { typeRepository.save(any()) } answers { call -> + (call.invocation.args[0] as TypeEntity).copy(id = uuid) + } + coEvery { typeDescriptionRepository.upsert(any(), any()) } answers { call -> + TypeDescriptionEntity( + typeId = call.invocation.args[0] as UUID, + description = call.invocation.args[1] as String, + ) + } // when - service.addType(entity) + val result = service.addType("name", "description") // then - coVerify(exactly = 1) { repository.save(any()) } + coVerify(exactly = 1) { typeRepository.save(any()) } + coVerify(exactly = 1) { typeDescriptionRepository.upsert(any(), any()) } + + assertThat(result.id).isEqualToUuid("00000000-0000-0000-0000-000000000000") + assertThat(result.timestamp).isEqualTo(timestamp) + assertThat(result.name).isEqualTo("name") + assertThat(result.description).isEqualTo("description") } @Test - fun `add new type exception`() = runTest { + fun `add new type - fail this should never happen save not updating id`() = runTest { // given - val entity: TypeEntity = mockk() + coEvery { typeRepository.save(any()) } answers { call -> call.invocation.args[0] as TypeEntity } - coEvery { repository.save(any()) } throws DuplicateKeyException("duplicate key") - - // then exception - assertFailsWith { - service.addType(entity) + // when exception + val response: ResponseStatusException = assertFailsWith { + service.addType("name", "description") } + + // then + coVerify(exactly = 1) { typeRepository.save(any()) } + coVerify(exactly = 0) { typeDescriptionRepository.upsert(any(), any()) } + + assertThat(response.statusCode).isEqualTo(EXPECTATION_FAILED) + } + + @Test + fun `add new type - fail duplicate key`() = runTest { + // given + coEvery { typeRepository.save(any()) } throws DuplicateKeyException("duplicate key") + + // when exception + val response: ResponseStatusException = assertFailsWith { + service.addType("name", "description") + } + + // then + coVerify(exactly = 1) { typeRepository.save(any()) } + coVerify(exactly = 0) { typeDescriptionRepository.upsert(any(), any()) } + + assertThat(response.statusCode).isEqualTo(CONFLICT) + } + + @Test + fun `get type - success`() = runTest { + // given + coEvery { typeRepository.findTypeWithDescription(any()) } answers { call -> + TypeWithDescription( + id = call.invocation.args[0] as UUID, + timestamp = timestamp, + name = "name", + description = "description", + ) + } + + // when + val result = service.getType(uuid) + + // then + coVerify(exactly = 1) { typeRepository.findTypeWithDescription(any()) } + + assertThat(result.id).isEqualToUuid("00000000-0000-0000-0000-000000000000") + assertThat(result.timestamp).isEqualTo(timestamp) + assertThat(result.name).isEqualTo("name") + assertThat(result.description).isEqualTo("description") + } + + @Test + fun `get type - fail`() = runTest { + // given + coEvery { typeRepository.findTypeWithDescription(any()) } returns null + + // when exception + val response: ResponseStatusException = assertFailsWith { + service.getType(uuid) + } + + // then + coVerify(exactly = 1) { typeRepository.findTypeWithDescription(any()) } + + assertThat(response.statusCode).isEqualTo(NOT_FOUND) + } + + @Test + fun `update type - success`() = runTest { + // given + coEvery { typeRepository.findById(any()) } answers { call -> + TypeEntity( + id = call.invocation.args[0] as UUID, + timestamp = timestamp, + name = "name", + ) + } + coEvery { typeRepository.save(any()) } answers { call -> + call.invocation.args[0] as TypeEntity + } + coEvery { typeDescriptionRepository.findById(any()) } answers { call -> + TypeDescriptionEntity( + typeId = call.invocation.args[0] as UUID, + description = "description", + ) + } + coEvery { typeDescriptionRepository.save(any()) } answers { call -> + call.invocation.args[0] as TypeDescriptionEntity + } + + // when + val result = service.updateType(uuid, "new-name", "new-description") + + // then + coVerify(exactly = 1) { typeRepository.findById(any()) } + coVerify(exactly = 1) { typeRepository.save(any()) } + coVerify(exactly = 1) { typeDescriptionRepository.findById(any()) } + coVerify(exactly = 1) { typeDescriptionRepository.save(any()) } + + assertThat(result.id).isEqualToUuid("00000000-0000-0000-0000-000000000000") + assertThat(result.timestamp).isEqualTo(timestamp) + assertThat(result.name).isEqualTo("new-name") + assertThat(result.description).isEqualTo("new-description") + } + + @Test + fun `update type - success no change`() = runTest { + // given + coEvery { typeRepository.findById(any()) } answers { call -> + TypeEntity( + id = call.invocation.args[0] as UUID, + timestamp = timestamp, + name = "name", + ) + } + coEvery { typeDescriptionRepository.findById(any()) } answers { call -> + TypeDescriptionEntity( + typeId = call.invocation.args[0] as UUID, + description = "description", + ) + } + + // when exception + val response: ResponseStatusException = assertFailsWith { + service.updateType(uuid, "name", "description") + } + + // then + coVerify(exactly = 1) { typeRepository.findById(any()) } + coVerify(exactly = 0) { typeRepository.save(any()) } + coVerify(exactly = 1) { typeDescriptionRepository.findById(any()) } + coVerify(exactly = 0) { typeDescriptionRepository.save(any()) } + + assertThat(response.statusCode).isEqualTo(NO_CONTENT) + } + + @Test + fun `update type - fail type dont exist`() = runTest { + // given + coEvery { typeRepository.findById(any()) } returns null + + // when exception + val response: ResponseStatusException = assertFailsWith { + service.updateType(uuid, "name", "description") + } + + // then + coVerify(exactly = 1) { typeRepository.findById(any()) } + coVerify(exactly = 0) { typeRepository.save(any()) } + coVerify(exactly = 0) { typeDescriptionRepository.findById(any()) } + coVerify(exactly = 0) { typeDescriptionRepository.save(any()) } + + assertThat(response.statusCode).isEqualTo(NOT_FOUND) + } + + @Test + fun `update type - fail type description dont exist`() = runTest { + // given + coEvery { typeRepository.findById(any()) } answers { call -> + TypeEntity( + id = call.invocation.args[0] as UUID, + timestamp = timestamp, + name = "name", + ) + } + coEvery { typeDescriptionRepository.findById(any()) } returns null + + // when exception + val response: ResponseStatusException = assertFailsWith { + service.updateType(uuid, "name", "description") + } + + // then + coVerify(exactly = 1) { typeRepository.findById(any()) } + coVerify(exactly = 0) { typeRepository.save(any()) } + coVerify(exactly = 1) { typeDescriptionRepository.findById(any()) } + coVerify(exactly = 0) { typeDescriptionRepository.save(any()) } + + assertThat(response.statusCode).isEqualTo(NOT_FOUND) + } + + @Test + fun `update type - fail name already exists`() = runTest { + // given + coEvery { typeRepository.findById(any()) } answers { call -> + TypeEntity( + id = call.invocation.args[0] as UUID, + timestamp = timestamp, + name = "name", + ) + } + coEvery { typeRepository.save(any()) } throws DuplicateKeyException("duplicate key") + + // when exception + val response: ResponseStatusException = assertFailsWith { + service.updateType(uuid, "taken-name", "description") + } + + // then + coVerify(exactly = 1) { typeRepository.findById(any()) } + coVerify(exactly = 1) { typeRepository.save(any()) } + coVerify(exactly = 0) { typeDescriptionRepository.findById(any()) } + coVerify(exactly = 0) { typeDescriptionRepository.save(any()) } + + assertThat(response.statusCode).isEqualTo(CONFLICT) } } diff --git a/src/test/kotlin/ltd/hlaeja/util/MappingKtTest.kt b/src/test/kotlin/ltd/hlaeja/util/MappingKtTest.kt index f04b40c..5fe7cd3 100644 --- a/src/test/kotlin/ltd/hlaeja/util/MappingKtTest.kt +++ b/src/test/kotlin/ltd/hlaeja/util/MappingKtTest.kt @@ -1,6 +1,7 @@ package ltd.hlaeja.util import io.mockk.every +import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.unmockkStatic import java.time.LocalDateTime @@ -8,8 +9,11 @@ import java.time.ZoneId import java.time.ZonedDateTime import java.util.UUID import kotlin.test.Test +import ltd.hlaeja.dto.TypeWithDescription +import ltd.hlaeja.entity.DeviceEntity import ltd.hlaeja.entity.NodeEntity import ltd.hlaeja.entity.TypeEntity +import ltd.hlaeja.jwt.service.PrivateJwtService import ltd.hlaeja.library.deviceRegistry.Node import ltd.hlaeja.library.deviceRegistry.Type import ltd.hlaeja.test.isEqualToUuid @@ -40,23 +44,82 @@ class MappingKtTest { inner class TypeMapping { @Test - fun `request to entity successful`() { + fun `request to type entity successful`() { // given + val id = UUID.fromString("00000000-0000-0000-0000-000000000001") val request = Type.Request( - "test", + "name", + "description", ) // when - val result = request.toTypeEntity() + val entity = request.toTypeEntity(id) // then - assertThat(result.id).isNull() - assertThat(result.timestamp.toString()).isEqualTo("2000-01-01T00:00:00.000000001Z[UTC]") - assertThat(result.name).isEqualTo("test") + assertThat(entity.id).isEqualToUuid("00000000-0000-0000-0000-000000000001") + assertThat(entity.timestamp).isEqualTo(timestamp) + assertThat(entity.name).isEqualTo("name") } @Test - fun `entity to response successful`() { + fun `request to type description entity successful`() { + // given + val id = UUID.fromString("00000000-0000-0000-0000-000000000001") + val request = Type.Request( + "name", + "description", + ) + + // when + val entity = request.toTypeDescriptionEntity(id) + + // then + assertThat(entity.typeId).isEqualToUuid("00000000-0000-0000-0000-000000000001") + assertThat(entity.description).isEqualTo("description") + } + + @Test + fun `type with description to response successful`() { + // given + val typeWithDescription = TypeWithDescription( + UUID.fromString("00000000-0000-0000-0000-000000000001"), + timestamp, + "name", + "description", + ) + + // when + val response = typeWithDescription.toTypeResponse() + + // then + assertThat(response.id).isEqualToUuid("00000000-0000-0000-0000-000000000001") + assertThat(response.timestamp).isEqualTo(timestamp) + assertThat(response.name).isEqualTo("name") + assertThat(response.description).isEqualTo("description") + } + + @Test + fun `type with description to response, description null successful`() { + // given + val typeWithDescription = TypeWithDescription( + UUID.fromString("00000000-0000-0000-0000-000000000001"), + timestamp, + "name", + null, + ) + + // when + val response = typeWithDescription.toTypeResponse() + + // then + assertThat(response.id).isEqualToUuid("00000000-0000-0000-0000-000000000001") + assertThat(response.timestamp).isEqualTo(timestamp) + assertThat(response.name).isEqualTo("name") + assertThat(response.description).isEmpty() + } + + @Test + fun `type entity to response successful`() { // given val entity = TypeEntity( UUID.fromString("00000000-0000-0000-0000-000000000000"), @@ -65,20 +128,13 @@ class MappingKtTest { ) // when - val response = entity.toTypeResponse() + val response = entity.toTypesResponse() // then assertThat(response.id).isEqualToUuid("00000000-0000-0000-0000-000000000000") + assertThat(response.timestamp).isEqualTo(timestamp) assertThat(response.name).isEqualTo("name") } - - @Test - fun `entity to response exception`() { - // then exception - assertThrows(ResponseStatusException::class.java) { - TypeEntity(null, timestamp, "name").toTypeResponse() - } - } } @Nested @@ -98,7 +154,7 @@ class MappingKtTest { // then assertThat(result.id).isNull() - assertThat(result.timestamp.toString()).isEqualTo("2000-01-01T00:00:00.000000001Z[UTC]") + assertThat(result.timestamp).isEqualTo(timestamp) assertThat(result.client.toString()).isEqualTo("00000000-0000-0000-0000-000000000001") assertThat(result.device.toString()).isEqualTo("00000000-0000-0000-0000-000000000002") assertThat(result.name).isEqualTo("test") @@ -189,4 +245,48 @@ class MappingKtTest { assertThat(exception.message).isEqualTo("417 EXPECTATION_FAILED") } } + + @Nested + inner class DeviceMapping { + + val jwtService: PrivateJwtService = mockk() + + @Test + fun `entity to identity response successful`() { + // given + val entity = DeviceEntity( + UUID.fromString("00000000-0000-0000-0000-000000000001"), + timestamp, + UUID.fromString("00000000-0000-0000-0000-000000000002"), + ) + + every { jwtService.sign(any()) } returns "header.payload.signature" + + // when + val result = entity.toDeviceResponse(jwtService) + + // then + assertThat(result.id).isEqualToUuid("00000000-0000-0000-0000-000000000001") + assertThat(result.type).isEqualToUuid("00000000-0000-0000-0000-000000000002") + assertThat(result.identity).isEqualTo("header.payload.signature") + } + + @Test + fun `entity to identity response exception`() { + // given + val entity = DeviceEntity( + null, + timestamp, + UUID.fromString("00000000-0000-0000-0000-000000000002"), + ) + + // then exception + val exception = assertThrows(ResponseStatusException::class.java) { + entity.toDeviceResponse(jwtService) + } + + // then + assertThat(exception.message).isEqualTo("417 EXPECTATION_FAILED") + } + } }