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
This commit is contained in:
2025-03-11 07:51:40 +01:00
parent 53db4408e2
commit 0c4a5d0af6
21 changed files with 947 additions and 84 deletions

View File

@@ -1,7 +1,20 @@
### add type by name ### add type
POST {{hostname}}/type POST {{hostname}}/type
Content-Type: application/json 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."
} }

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -2,6 +2,7 @@ package ltd.hlaeja.controller
import ltd.hlaeja.library.deviceRegistry.Type import ltd.hlaeja.library.deviceRegistry.Type
import ltd.hlaeja.test.container.PostgresContainer import ltd.hlaeja.test.container.PostgresContainer
import ltd.hlaeja.test.isEqualToUuid
import org.assertj.core.api.SoftAssertions import org.assertj.core.api.SoftAssertions
import org.assertj.core.api.junit.jupiter.InjectSoftAssertions import org.assertj.core.api.junit.jupiter.InjectSoftAssertions
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension 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.Nested
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith 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
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
import org.springframework.boot.test.web.server.LocalServerPort import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.http.HttpStatus.CONFLICT 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.WebTestClient
import org.springframework.test.web.reactive.server.expectBody import org.springframework.test.web.reactive.server.expectBody
@@ -40,9 +45,11 @@ class TypeEndpoint {
@Test @Test
fun `added type - success`() { fun `added type - success`() {
// given // given
val name = "Thing 5" val name = "Thing 5 v1"
val description = "Thing 5 description"
val request = Type.Request( val request = Type.Request(
name = name, name = name,
description = description,
) )
// when // when
@@ -50,11 +57,12 @@ class TypeEndpoint {
// then // then
result.expectStatus() result.expectStatus()
.isOk .isCreated
.expectBody<Type.Response>() .expectBody<Type.Response>()
.consumeWith { .consumeWith {
softly.assertThat(it.responseBody?.id?.version()).isEqualTo(7) softly.assertThat(it.responseBody?.id?.version()).isEqualTo(7)
softly.assertThat(it.responseBody?.name).isEqualTo(name) softly.assertThat(it.responseBody?.name).isEqualTo(name)
softly.assertThat(it.responseBody?.description).isEqualTo(description)
} }
} }
@@ -63,6 +71,7 @@ class TypeEndpoint {
// given // given
val request = Type.Request( val request = Type.Request(
name = "Thing 1 v1", name = "Thing 1 v1",
description = "Thing 1 description",
) )
// when // when
@@ -71,5 +80,212 @@ class TypeEndpoint {
// then // then
result.expectStatus().isEqualTo(CONFLICT) 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<Type.Response>()
.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<Type.Response>()
.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
}
} }
} }

View File

@@ -1,6 +1,6 @@
package ltd.hlaeja.controller package ltd.hlaeja.controller
import ltd.hlaeja.library.deviceRegistry.Type import ltd.hlaeja.library.deviceRegistry.Types
import ltd.hlaeja.test.container.PostgresContainer import ltd.hlaeja.test.container.PostgresContainer
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
@@ -36,7 +36,7 @@ class TypesEndpoint {
// then // then
result.expectStatus().isOk() result.expectStatus().isOk()
.expectBody<List<Type.Response>>() .expectBody<List<Types.Response>>()
.consumeWith { .consumeWith {
assertThat(it.responseBody?.size).isEqualTo(4) assertThat(it.responseBody?.size).isEqualTo(4)
} }
@@ -55,7 +55,7 @@ class TypesEndpoint {
// then // then
result.expectStatus().isOk() result.expectStatus().isOk()
.expectBody<List<Type.Response>>() .expectBody<List<Types.Response>>()
.consumeWith { .consumeWith {
assertThat(it.responseBody?.size).isEqualTo(expected) assertThat(it.responseBody?.size).isEqualTo(expected)
} }
@@ -84,7 +84,7 @@ class TypesEndpoint {
// then // then
result.expectStatus().isOk() result.expectStatus().isOk()
.expectBody<List<Type.Response>>() .expectBody<List<Types.Response>>()
.consumeWith { .consumeWith {
assertThat(it.responseBody?.size).isEqualTo(expected) assertThat(it.responseBody?.size).isEqualTo(expected)
} }
@@ -120,7 +120,7 @@ class TypesEndpoint {
// then // then
result.expectStatus().isOk() result.expectStatus().isOk()
.expectBody<List<Type.Response>>() .expectBody<List<Types.Response>>()
.consumeWith { .consumeWith {
assertThat(it.responseBody?.size).isEqualTo(expected) assertThat(it.responseBody?.size).isEqualTo(expected)
} }
@@ -144,7 +144,7 @@ class TypesEndpoint {
// then // then
result.expectStatus().isOk() result.expectStatus().isOk()
.expectBody<List<Type.Response>>() .expectBody<List<Types.Response>>()
.consumeWith { .consumeWith {
assertThat(it.responseBody?.size).isEqualTo(expected) assertThat(it.responseBody?.size).isEqualTo(expected)
} }
@@ -185,7 +185,7 @@ class TypesEndpoint {
// then // then
result.expectStatus().isOk() result.expectStatus().isOk()
.expectBody<List<Type.Response>>() .expectBody<List<Types.Response>>()
.consumeWith { .consumeWith {
assertThat(it.responseBody?.size).isEqualTo(expected) assertThat(it.responseBody?.size).isEqualTo(expected)
} }

View File

@@ -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-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'); ('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) 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), 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), ('00000000-0000-0000-0002-000000000002'::uuid, '2000-01-01 00:00:00.000001 +00:00', '00000000-0000-0000-0001-000000000001'::uuid),

View File

@@ -3,6 +3,7 @@
-- Truncate tables -- Truncate tables
TRUNCATE TABLE nodes CASCADE; TRUNCATE TABLE nodes CASCADE;
TRUNCATE TABLE devices CASCADE; TRUNCATE TABLE devices CASCADE;
TRUNCATE TABLE type_descriptions CASCADE;
TRUNCATE TABLE types CASCADE; TRUNCATE TABLE types CASCADE;
-- Enable triggers on the account table -- Enable triggers on the account table

View File

@@ -31,6 +31,20 @@ CREATE TABLE IF NOT EXISTS public.types
CONSTRAINT pk_contact_types PRIMARY KEY (id) 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 -- Table: public.devices
CREATE TABLE IF NOT EXISTS public.devices CREATE TABLE IF NOT EXISTS public.devices

View File

@@ -1,11 +1,16 @@
package ltd.hlaeja.controller package ltd.hlaeja.controller
import java.util.UUID
import ltd.hlaeja.library.deviceRegistry.Type import ltd.hlaeja.library.deviceRegistry.Type
import ltd.hlaeja.service.TypeService import ltd.hlaeja.service.TypeService
import ltd.hlaeja.util.toTypeEntity
import ltd.hlaeja.util.toTypeResponse 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.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@@ -14,7 +19,19 @@ class TypeController(
) { ) {
@PostMapping("/type") @PostMapping("/type")
@ResponseStatus(CREATED)
suspend fun addType( suspend fun addType(
@RequestBody register: Type.Request, @RequestBody request: Type.Request,
): Type.Response = service.addType(register.toTypeEntity()).toTypeResponse() ): 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()
} }

View File

@@ -3,9 +3,9 @@ package ltd.hlaeja.controller
import jakarta.validation.constraints.Min import jakarta.validation.constraints.Min
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map 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.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.GetMapping
import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RestController 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) page: Int = DEFAULT_PAGE,
@PathVariable(required = false) @Min(1) show: Int = DEFAULT_SIZE, @PathVariable(required = false) @Min(1) show: Int = DEFAULT_SIZE,
@PathVariable(required = false) filter: String? = null, @PathVariable(required = false) filter: String? = null,
): Flow<Type.Response> = service.getTypes((page - 1) * show, show, filter) ): Flow<Types.Response> = service.getTypes((page - 1) * show, show, filter)
.map { it.toTypeResponse() } .map { it.toTypesResponse() }
} }

View File

@@ -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?,
)

View File

@@ -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,
)

View File

@@ -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<TypeDescriptionEntity, UUID> {
@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
}

View File

@@ -2,8 +2,8 @@ package ltd.hlaeja.repository
import java.util.UUID import java.util.UUID
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import ltd.hlaeja.dto.TypeWithDescription
import ltd.hlaeja.entity.TypeEntity import ltd.hlaeja.entity.TypeEntity
import org.springframework.data.r2dbc.repository.Query import org.springframework.data.r2dbc.repository.Query
import org.springframework.data.repository.kotlin.CoroutineCrudRepository import org.springframework.data.repository.kotlin.CoroutineCrudRepository
import org.springframework.data.repository.query.Param import org.springframework.data.repository.query.Param
@@ -24,4 +24,16 @@ interface TypeRepository : CoroutineCrudRepository<TypeEntity, UUID> {
@Param("offset") offset: Int, @Param("offset") offset: Int,
@Param("limit") limit: Int, @Param("limit") limit: Int,
): Flow<TypeEntity> ): Flow<TypeEntity>
@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?
} }

View File

@@ -1,12 +1,21 @@
package ltd.hlaeja.service package ltd.hlaeja.service
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import java.time.ZonedDateTime
import java.util.UUID
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import ltd.hlaeja.dto.TypeWithDescription
import ltd.hlaeja.entity.TypeDescriptionEntity
import ltd.hlaeja.entity.TypeEntity import ltd.hlaeja.entity.TypeEntity
import ltd.hlaeja.repository.TypeDescriptionRepository
import ltd.hlaeja.repository.TypeRepository import ltd.hlaeja.repository.TypeRepository
import org.springframework.dao.DuplicateKeyException import org.springframework.dao.DataIntegrityViolationException
import org.springframework.http.HttpStatus 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.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.server.ResponseStatusException import org.springframework.web.server.ResponseStatusException
private val log = KotlinLogging.logger {} private val log = KotlinLogging.logger {}
@@ -14,6 +23,7 @@ private val log = KotlinLogging.logger {}
@Service @Service
class TypeService( class TypeService(
private val typeRepository: TypeRepository, private val typeRepository: TypeRepository,
private val typeDescriptionRepository: TypeDescriptionRepository,
) { ) {
fun getTypes( fun getTypes(
@@ -25,13 +35,87 @@ class TypeService(
else -> typeRepository.findAll(page, show) else -> typeRepository.findAll(page, show)
} }
@Transactional
suspend fun addType( suspend fun addType(
entity: TypeEntity, name: String,
): TypeEntity = try { description: String,
typeRepository.save(entity) ): TypeWithDescription = try {
.also { log.debug { "Added new type: $it.id" } } val savedType = typeRepository.save(
} catch (e: DuplicateKeyException) { TypeEntity(timestamp = ZonedDateTime.now(), name = name),
log.warn { e.localizedMessage } ).also { log.debug { "Added new type: ${it.id}" } }
throw ResponseStatusException(HttpStatus.CONFLICT) 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")
}
}
} }
} }

View File

@@ -1,22 +1,43 @@
package ltd.hlaeja.util package ltd.hlaeja.util
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.util.UUID
import ltd.hlaeja.entity.DeviceEntity import ltd.hlaeja.entity.DeviceEntity
import ltd.hlaeja.entity.NodeEntity import ltd.hlaeja.entity.NodeEntity
import ltd.hlaeja.entity.TypeEntity import ltd.hlaeja.entity.TypeEntity
import ltd.hlaeja.dto.TypeWithDescription
import ltd.hlaeja.entity.TypeDescriptionEntity
import ltd.hlaeja.jwt.service.PrivateJwtService import ltd.hlaeja.jwt.service.PrivateJwtService
import ltd.hlaeja.library.deviceRegistry.Device import ltd.hlaeja.library.deviceRegistry.Device
import ltd.hlaeja.library.deviceRegistry.Identity import ltd.hlaeja.library.deviceRegistry.Identity
import ltd.hlaeja.library.deviceRegistry.Node import ltd.hlaeja.library.deviceRegistry.Node
import ltd.hlaeja.library.deviceRegistry.Type import ltd.hlaeja.library.deviceRegistry.Type
import ltd.hlaeja.library.deviceRegistry.Types
import org.springframework.http.HttpStatus.EXPECTATION_FAILED import org.springframework.http.HttpStatus.EXPECTATION_FAILED
import org.springframework.web.server.ResponseStatusException 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( fun Type.Request.toTypeDescriptionEntity(id: UUID): TypeDescriptionEntity = TypeDescriptionEntity(
id ?: throw ResponseStatusException(EXPECTATION_FAILED), typeId = id,
name, 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( fun Node.Request.toEntity(): NodeEntity = NodeEntity(

View File

@@ -8,7 +8,7 @@ import java.time.ZoneId
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.util.UUID import java.util.UUID
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import ltd.hlaeja.entity.TypeEntity import ltd.hlaeja.dto.TypeWithDescription
import ltd.hlaeja.library.deviceRegistry.Type import ltd.hlaeja.library.deviceRegistry.Type
import ltd.hlaeja.service.TypeService import ltd.hlaeja.service.TypeService
import ltd.hlaeja.test.isEqualToUuid import ltd.hlaeja.test.isEqualToUuid
@@ -34,16 +34,59 @@ class TypeControllerTest {
@Test @Test
fun `add type`() = runTest { fun `add type`() = runTest {
// given // given
val request = Type.Request("name") val request = Type.Request("name", "description")
coEvery { service.addType(any()) } returns TypeEntity(id, timestamp, "name") coEvery { service.addType(any(), any()) } returns TypeWithDescription(id, timestamp, "name", "description")
// when // when
val response = controller.addType(request) val response = controller.addType(request)
// then // 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.id).isEqualToUuid("00000000-0000-0000-0000-000000000000")
assertThat(response.name).isEqualTo("name") 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")
} }
} }

View File

@@ -19,8 +19,10 @@ import org.junit.jupiter.api.Test
class TypesControllerTest { class TypesControllerTest {
companion object { companion object {
val id = UUID.fromString("00000000-0000-0000-0000-000000000000") const val NAME: String = "name"
val timestamp = ZonedDateTime.of(LocalDateTime.of(2000, 1, 1, 0, 0, 0, 1), ZoneId.of("UTC")) 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() val service: TypeService = mockk()
@@ -35,7 +37,9 @@ class TypesControllerTest {
@Test @Test
fun `get all types`() = runTest { fun `get all types`() = runTest {
// given // 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 // when
val response = controller.getTypes().single() val response = controller.getTypes().single()
@@ -43,7 +47,8 @@ class TypesControllerTest {
// then // then
verify(exactly = 1) { service.getTypes(0, 25, null) } verify(exactly = 1) { service.getTypes(0, 25, null) }
assertThat(response.id).isEqualToUuid("00000000-0000-0000-0000-000000000000") assertThat(response.id).isEqualToUuid(NIL_UUID)
assertThat(response.name).isEqualTo("name") assertThat(response.name).isEqualTo(NAME)
assertThat(response.timestamp).isEqualTo(timestamp)
} }
} }

View File

@@ -4,90 +4,321 @@ import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkStatic
import io.mockk.verify import io.mockk.verify
import java.time.Instant import java.time.LocalDateTime
import java.time.ZoneId import java.time.ZoneId
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.util.UUID import java.util.UUID
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import ltd.hlaeja.dto.TypeWithDescription
import ltd.hlaeja.entity.TypeDescriptionEntity
import ltd.hlaeja.entity.TypeEntity import ltd.hlaeja.entity.TypeEntity
import ltd.hlaeja.repository.TypeDescriptionRepository
import ltd.hlaeja.repository.TypeRepository 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.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.springframework.dao.DuplicateKeyException 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 import org.springframework.web.server.ResponseStatusException
class TypeServiceTest { class TypeServiceTest {
companion object { 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 uuid = UUID.fromString("00000000-0000-0000-0000-000000000000")
} }
val repository: TypeRepository = mockk() val typeRepository: TypeRepository = mockk()
val typeDescriptionRepository: TypeDescriptionRepository = mockk()
lateinit var service: TypeService lateinit var service: TypeService
@BeforeEach @BeforeEach
fun setUp() { fun setUp() {
service = TypeService(repository) service = TypeService(typeRepository, typeDescriptionRepository)
mockkStatic(ZonedDateTime::class)
every { ZonedDateTime.now() } returns timestamp
}
@AfterEach
fun tearDown() {
unmockkStatic(ZonedDateTime::class)
} }
@Test @Test
fun `get all types`() { fun `get all types`() {
// given // given
every { repository.findAll(any(), any()) } returns flowOf(mockk<TypeEntity>()) every { typeRepository.findAll(any(), any()) } returns flowOf(mockk<TypeEntity>())
// when // when
service.getTypes(1, 10, null) service.getTypes(1, 10, null)
// then // then
verify(exactly = 1) { repository.findAll(1, 10) } verify(exactly = 1) { typeRepository.findAll(1, 10) }
verify(exactly = 0) { repository.findAllContaining(any(), any(), any()) } verify(exactly = 0) { typeRepository.findAllContaining(any(), any(), any()) }
} }
@Test @Test
fun `get all types with filter`() { fun `get all types with filter`() {
// given // given
every { repository.findAllContaining(any(), any(), any()) } returns flowOf(mockk<TypeEntity>()) every { typeRepository.findAllContaining(any(), any(), any()) } returns flowOf(mockk<TypeEntity>())
// when // when
service.getTypes(1, 10, "abc") service.getTypes(1, 10, "abc")
// then // then
verify(exactly = 1) { repository.findAllContaining("%abc%", 1, 10) } verify(exactly = 1) { typeRepository.findAllContaining("%abc%", 1, 10) }
verify(exactly = 0) { repository.findAll(any(), any()) } verify(exactly = 0) { typeRepository.findAll(any(), any()) }
} }
@Test @Test
fun `add new type success`() = runTest { fun `add new type success`() = runTest {
// given // given
val entity = TypeEntity( coEvery { typeRepository.save(any()) } answers { call ->
null, (call.invocation.args[0] as TypeEntity).copy(id = uuid)
timestamp, }
"name", coEvery { typeDescriptionRepository.upsert(any(), any()) } answers { call ->
TypeDescriptionEntity(
typeId = call.invocation.args[0] as UUID,
description = call.invocation.args[1] as String,
) )
}
coEvery { repository.save(any()) } answers { call -> (call.invocation.args[0] as TypeEntity).copy(id = uuid) }
// when // when
service.addType(entity) val result = service.addType("name", "description")
// then // 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 @Test
fun `add new type exception`() = runTest { fun `add new type - fail this should never happen save not updating id`() = runTest {
// given // 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") // when exception
val response: ResponseStatusException = assertFailsWith<ResponseStatusException> {
service.addType("name", "description")
}
// then exception // then
assertFailsWith<ResponseStatusException> { coVerify(exactly = 1) { typeRepository.save(any()) }
service.addType(entity) 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<ResponseStatusException> {
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<ResponseStatusException> {
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<ResponseStatusException> {
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<ResponseStatusException> {
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<ResponseStatusException> {
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<ResponseStatusException> {
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)
} }
} }

View File

@@ -1,6 +1,7 @@
package ltd.hlaeja.util package ltd.hlaeja.util
import io.mockk.every import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic import io.mockk.mockkStatic
import io.mockk.unmockkStatic import io.mockk.unmockkStatic
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -8,8 +9,11 @@ import java.time.ZoneId
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.util.UUID import java.util.UUID
import kotlin.test.Test import kotlin.test.Test
import ltd.hlaeja.dto.TypeWithDescription
import ltd.hlaeja.entity.DeviceEntity
import ltd.hlaeja.entity.NodeEntity import ltd.hlaeja.entity.NodeEntity
import ltd.hlaeja.entity.TypeEntity import ltd.hlaeja.entity.TypeEntity
import ltd.hlaeja.jwt.service.PrivateJwtService
import ltd.hlaeja.library.deviceRegistry.Node import ltd.hlaeja.library.deviceRegistry.Node
import ltd.hlaeja.library.deviceRegistry.Type import ltd.hlaeja.library.deviceRegistry.Type
import ltd.hlaeja.test.isEqualToUuid import ltd.hlaeja.test.isEqualToUuid
@@ -40,23 +44,82 @@ class MappingKtTest {
inner class TypeMapping { inner class TypeMapping {
@Test @Test
fun `request to entity successful`() { fun `request to type entity successful`() {
// given // given
val id = UUID.fromString("00000000-0000-0000-0000-000000000001")
val request = Type.Request( val request = Type.Request(
"test", "name",
"description",
) )
// when // when
val result = request.toTypeEntity() val entity = request.toTypeEntity(id)
// then // then
assertThat(result.id).isNull() assertThat(entity.id).isEqualToUuid("00000000-0000-0000-0000-000000000001")
assertThat(result.timestamp.toString()).isEqualTo("2000-01-01T00:00:00.000000001Z[UTC]") assertThat(entity.timestamp).isEqualTo(timestamp)
assertThat(result.name).isEqualTo("test") assertThat(entity.name).isEqualTo("name")
} }
@Test @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 // given
val entity = TypeEntity( val entity = TypeEntity(
UUID.fromString("00000000-0000-0000-0000-000000000000"), UUID.fromString("00000000-0000-0000-0000-000000000000"),
@@ -65,20 +128,13 @@ class MappingKtTest {
) )
// when // when
val response = entity.toTypeResponse() val response = entity.toTypesResponse()
// then // then
assertThat(response.id).isEqualToUuid("00000000-0000-0000-0000-000000000000") assertThat(response.id).isEqualToUuid("00000000-0000-0000-0000-000000000000")
assertThat(response.timestamp).isEqualTo(timestamp)
assertThat(response.name).isEqualTo("name") assertThat(response.name).isEqualTo("name")
} }
@Test
fun `entity to response exception`() {
// then exception
assertThrows(ResponseStatusException::class.java) {
TypeEntity(null, timestamp, "name").toTypeResponse()
}
}
} }
@Nested @Nested
@@ -98,7 +154,7 @@ class MappingKtTest {
// then // then
assertThat(result.id).isNull() 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.client.toString()).isEqualTo("00000000-0000-0000-0000-000000000001")
assertThat(result.device.toString()).isEqualTo("00000000-0000-0000-0000-000000000002") assertThat(result.device.toString()).isEqualTo("00000000-0000-0000-0000-000000000002")
assertThat(result.name).isEqualTo("test") assertThat(result.name).isEqualTo("test")
@@ -189,4 +245,48 @@ class MappingKtTest {
assertThat(exception.message).isEqualTo("417 EXPECTATION_FAILED") 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")
}
}
} }