11 Commits

Author SHA1 Message Date
528636de5d [RELEASE] - release version: 0.5.0 2025-04-05 12:55:24 +02:00
d90a716df7 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
2025-04-04 11:31:49 +02:00
53db4408e2 update get type(s) for pagination and filter
- update TypesEndpoint for all new endpoints
- update application to use properties for sql script
- update types controller
  - update types.http for all types of types calls
  - update test mocking for service
  - update getTypes to use filter, page, and show in TypesController
  - add validation dependency
- update unit test for uuid assertion from test library in
  - remove UUIDAssert.kt
  - update IdentityControllerTest
  - update MappingKtTest
  - update NodeControllerTest
  - update TypeControllerTest
  - update TypesControllerTest
  - add test library dependency
- update getTypes to handle filter limit and offset in TypeService
- update TypeRepository
  - add findAllContaining with filter, limit and offset
  - add findAll with limit and offset
- update type database with specific name key
- split Type and Types
2025-03-11 06:02:16 +01:00
10b95057e5 Update Node
- add error handing for
  - foreign key constraint
  - unique constraint
- Update nodes database
2025-03-05 21:34:37 +01:00
1aba25b9b3 update release.sh to handel sql move to version catalog 2025-03-05 18:33:59 +01:00
f5038a7e9e add end-to-end test
- add end-to-end test for identity controller
  - add IdentityEndpoint

- add end-to-end test for node controller
  - add NodeEndpoint
  - update test container sql files

- add end-to-end test for device controller
  - add DeviceEndpoint
  - add identity in first-device.data
  - add .data to .editorconfig
  - update test container sql files

- add end-to-end test for type controller
  - add TypeEndpoint
  - update test container sql files

- prepare for end-to-end test
  - add sql files for postgres container
  - update to build.gradle.kts
    - add integration test dependencies
    - update hlaeja dependency after name changes
  - update catalog version
  - move files from test to integration test
    - valid-private-key.pem
    - application.yml
    - ApplicationTest
2025-03-05 18:33:20 +01:00
f304d3d61a [RELEASE] - bump version 2025-01-02 07:24:15 +01:00
f52f1237a2 [RELEASE] - release version: 0.4.0 2025-01-02 07:24:12 +01:00
4130ba681c update addDevice to handle violates of foreign key in DeviceService 2025-01-02 06:53:20 +01:00
df9d2c59a4 replace local jwt with library version
- update DeviceController to handle hlaeja jwt instead of jwtService
- update mapper sign with hlaeja jwt instead of jwtService
- add dependency for hlaeja jwt
- remove dependencies for jjwt
- remove JwtService.kt
- remove PrivateKeyProvider.kt
- remove jwt key property explanation from additional-spring-configuration-metadata.json
2025-01-02 06:53:20 +01:00
7d4ebab8f8 [RELEASE] - bump version 2024-12-28 07:43:32 +01:00
55 changed files with 1838 additions and 362 deletions

View File

@@ -14,6 +14,10 @@ max_line_length = 1024
indent_size = 2 indent_size = 2
tab_width = 2 tab_width = 2
[*.data]
max_line_length = 1024
insert_final_newline = false
[*.bat] [*.bat]
end_of_line = crlf end_of_line = crlf

View File

@@ -9,28 +9,36 @@ plugins {
dependencies { dependencies {
implementation(hlaeja.fasterxml.jackson.module.kotlin) implementation(hlaeja.fasterxml.jackson.module.kotlin)
implementation(hlaeja.jjwt.api)
implementation(hlaeja.kotlin.logging) implementation(hlaeja.kotlin.logging)
implementation(hlaeja.kotlin.reflect) implementation(hlaeja.kotlin.reflect)
implementation(hlaeja.kotlinx.coroutines) implementation(hlaeja.kotlinx.coroutines)
implementation(hlaeja.library.hlaeja.common.messages) implementation(hlaeja.library.common.messages)
implementation(hlaeja.library.jwt)
implementation(hlaeja.springboot.starter.actuator) implementation(hlaeja.springboot.starter.actuator)
implementation(hlaeja.springboot.starter.r2dbc) implementation(hlaeja.springboot.starter.r2dbc)
implementation(hlaeja.springboot.starter.validation)
implementation(hlaeja.springboot.starter.webflux) implementation(hlaeja.springboot.starter.webflux)
runtimeOnly(hlaeja.jjwt.impl)
runtimeOnly(hlaeja.jjwt.jackson)
runtimeOnly(hlaeja.postgresql) runtimeOnly(hlaeja.postgresql)
runtimeOnly(hlaeja.postgresql.r2dbc) runtimeOnly(hlaeja.postgresql.r2dbc)
testImplementation(hlaeja.assertj.core) testImplementation(hlaeja.assertj.core)
testImplementation(hlaeja.library.test)
testImplementation(hlaeja.mockk) testImplementation(hlaeja.mockk)
testImplementation(hlaeja.projectreactor.reactor.test) testImplementation(hlaeja.projectreactor.reactor.test)
testImplementation(hlaeja.kotlin.test.junit5) testImplementation(hlaeja.kotlin.test.junit5)
testImplementation(hlaeja.kotlinx.coroutines.test) testImplementation(hlaeja.kotlinx.coroutines.test)
testImplementation(hlaeja.springboot.starter.test)
testRuntimeOnly(hlaeja.junit.platform.launcher) testRuntimeOnly(hlaeja.junit.platform.launcher)
integrationTestImplementation(hlaeja.assertj.core)
integrationTestImplementation(hlaeja.library.test)
integrationTestImplementation(hlaeja.projectreactor.reactor.test)
integrationTestImplementation(hlaeja.kotlin.test.junit5)
integrationTestImplementation(hlaeja.kotlinx.coroutines.test)
integrationTestImplementation(hlaeja.springboot.starter.test)
integrationTestRuntimeOnly(hlaeja.junit.platform.launcher)
} }
group = "ltd.hlaeja" group = "ltd.hlaeja"

View File

@@ -1,4 +1,4 @@
kotlin.code.style=official kotlin.code.style=official
version=0.3.0 version=0.5.0
catalog=0.7.0 catalog=0.10.0
container.port.host=9010 container.port.host=9010

View File

@@ -1,11 +1,20 @@
### let all types ### add type
GET {{hostname}}/types
### add type by name
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."
} }

17
http/types.http Normal file
View File

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

View File

@@ -71,6 +71,17 @@ snapshot_version() {
sed -i "s/\(version\s*=\s*\)[0-9.]*/\1$new_version-SNAPSHOT/" gradle.properties sed -i "s/\(version\s*=\s*\)[0-9.]*/\1$new_version-SNAPSHOT/" gradle.properties
} }
handle_sql_files() {
version=$(current_version)
sql_dir="sql"
version_dir="${sql_dir}/v${version}"
if [ -d "$sql_dir" ] && [ -n "$(ls -A $sql_dir/*.sql 2>/dev/null)" ]; then
mkdir -p "$version_dir"
mv "$sql_dir"/*.sql "$version_dir/"
git add "$sql_dir"
fi
}
# check and prepare for release # check and prepare for release
check_active_branch master check_active_branch master
check_uncommitted_changes check_uncommitted_changes
@@ -85,5 +96,6 @@ un_snapshot_version catalog
# release changes and prepare for next release # release changes and prepare for next release
commit_change "release version: $(current_version)" commit_change "release version: $(current_version)"
add_release_tag add_release_tag
handle_sql_files
snapshot_version snapshot_version
commit_change 'bump version' commit_change 'bump version'

5
sql/001-nodes.sql Normal file
View File

@@ -0,0 +1,5 @@
-- make device index unique
DROP INDEX IF EXISTS public.i_nodes_type;
CREATE UNIQUE INDEX IF NOT EXISTS i_nodes_device ON public.nodes (device);

View File

@@ -1,22 +1,6 @@
-- Table: public.types -- make name index unique order by name
-- DROP TABLE IF EXISTS public.types;
CREATE TABLE IF NOT EXISTS public.types DROP INDEX IF EXISTS types_name_key;
(
id UUID DEFAULT gen_uuid_v7(),
timestamp timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
name VARCHAR(50) UNIQUE NOT NULL,
CONSTRAINT pk_contact_types PRIMARY KEY (id)
);
ALTER TABLE IF EXISTS public.types CREATE UNIQUE INDEX IF NOT EXISTS types_name_key
OWNER to role_administrator; ON types (name ASC);
-- Revoke all permissions from existing roles
REVOKE ALL ON TABLE public.types FROM role_administrator, role_maintainer, role_support, role_service;
-- Grant appropriate permissions
GRANT ALL ON TABLE public.types TO role_administrator;
GRANT SELECT, INSERT, UPDATE ON TABLE public.types TO role_maintainer, role_service;
GRANT SELECT ON TABLE public.types TO role_support;

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;

27
sql/initial/002-types.sql Normal file
View File

@@ -0,0 +1,27 @@
-- Table: public.types
-- DROP TABLE IF EXISTS public.types;
CREATE TABLE IF NOT EXISTS public.types
(
id UUID DEFAULT gen_uuid_v7(),
timestamp timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
name VARCHAR(50) NOT NULL,
CONSTRAINT pk_contact_types PRIMARY KEY (id)
);
ALTER TABLE IF EXISTS public.types
OWNER to role_administrator;
-- Index: types_name_key
-- DROP INDEX IF EXISTS types_name_key;
CREATE UNIQUE INDEX IF NOT EXISTS types_name_key
ON types (name ASC);
-- Revoke all permissions from existing roles
REVOKE ALL ON TABLE public.types FROM role_administrator, role_maintainer, role_support, role_service;
-- Grant appropriate permissions
GRANT ALL ON TABLE public.types TO role_administrator;
GRANT SELECT, INSERT, UPDATE ON TABLE public.types TO role_maintainer, role_service;
GRANT SELECT ON TABLE public.types TO role_support;

View File

@@ -19,7 +19,7 @@ ALTER TABLE IF EXISTS public.nodes
-- Index: public.i_nodes_type -- Index: public.i_nodes_type
-- DROP INDEX IF EXISTS public.i_nodes_type; -- DROP INDEX IF EXISTS public.i_nodes_type;
CREATE INDEX IF NOT EXISTS i_nodes_device ON public.nodes (device); CREATE UNIQUE INDEX IF NOT EXISTS i_nodes_device ON public.nodes (device);
-- Revoke all permissions from existing roles -- Revoke all permissions from existing roles

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,10 @@
package ltd.hlaeja
@org.springframework.boot.test.context.SpringBootTest
class ApplicationTests {
@org.junit.jupiter.api.Test
fun contextLoads() {
// place holder
}
}

View File

@@ -0,0 +1,96 @@
package ltd.hlaeja.controller
import java.util.UUID
import ltd.hlaeja.library.deviceRegistry.Device
import ltd.hlaeja.test.compareToFile
import ltd.hlaeja.test.container.PostgresContainer
import ltd.hlaeja.test.isEqualToUuid
import org.assertj.core.api.SoftAssertions
import org.assertj.core.api.junit.jupiter.InjectSoftAssertions
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody
@PostgresContainer
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ExtendWith(SoftAssertionsExtension::class)
class DeviceEndpoint {
@InjectSoftAssertions
lateinit var softly: SoftAssertions
@LocalServerPort
var port: Int = 0
lateinit var webClient: WebTestClient
@BeforeEach
fun setup() {
webClient = WebTestClient.bindToServer().baseUrl("http://localhost:$port").build()
}
@Nested
inner class GetDevice {
@Test
fun `get account - success valid uuid`() {
// given
val uuid = UUID.fromString("00000000-0000-0000-0002-000000000001")
// when
val result = webClient.get().uri("/device-$uuid").exchange()
// then
result.expectStatus().isOk()
.expectBody<Device.Response>()
.consumeWith {
softly.assertThat(it.responseBody?.id).isEqualTo(uuid)
softly.assertThat(it.responseBody?.type).isEqualToUuid("00000000-0000-0000-0001-000000000001")
softly.assertThat(it.responseBody?.identity).compareToFile("identity/first-device.data")
}
}
@Test
fun `get account - fail non-existent uuid`() {
// given
val uuid = UUID.fromString("00000000-0000-0000-0002-000000000000")
// when
val result = webClient.get().uri("/device-$uuid").exchange()
// then
result.expectStatus().isNotFound
}
}
@Nested
inner class CreateDevice {
@Test
fun `added device - success`() {
// given
val uuid = UUID.fromString("00000000-0000-0000-0001-000000000003")
val request = Device.Request(
type = uuid,
)
// when
val result = webClient.post().uri("/device").bodyValue(request).exchange()
// then
result.expectStatus().isOk()
.expectBody<Device.Response>()
.consumeWith {
softly.assertThat(it.responseBody?.id?.version()).isEqualTo(7)
softly.assertThat(it.responseBody?.type).isEqualTo(uuid)
}
}
}
}

View File

@@ -0,0 +1,79 @@
package ltd.hlaeja.controller
import java.util.UUID
import ltd.hlaeja.library.deviceRegistry.Identity
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
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody
@PostgresContainer
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ExtendWith(SoftAssertionsExtension::class)
class IdentityEndpoint {
@InjectSoftAssertions
lateinit var softly: SoftAssertions
@LocalServerPort
var port: Int = 0
lateinit var webClient: WebTestClient
@BeforeEach
fun setup() {
webClient = WebTestClient.bindToServer().baseUrl("http://localhost:$port").build()
}
@Test
fun `get identity - success`() {
// given
val device = UUID.fromString("00000000-0000-0000-0002-000000000002")
// when
val result = webClient.get().uri("/identity/device-$device").exchange()
// then
result.expectStatus()
.isOk
.expectBody<Identity.Response>()
.consumeWith {
softly.assertThat(it.responseBody?.client).isEqualToUuid("00000000-0000-0000-0000-000000000000")
softly.assertThat(it.responseBody?.node).isEqualToUuid("00000000-0000-0000-0003-000000000001")
softly.assertThat(it.responseBody?.device).isEqualTo(device)
}
}
@Test
fun `get identity - fail device exist but not a node`() {
// given
val device = UUID.fromString("00000000-0000-0000-0002-000000000001")
// when
val result = webClient.get().uri("/identity/device-$device").exchange()
// then
result.expectStatus().isNotFound
}
@Test
fun `get identity - fail device dont exist`() {
// given
val device = UUID.fromString("00000000-0000-0000-0002-000000000000")
// when
val result = webClient.get().uri("/identity/device-$device").exchange()
// then
result.expectStatus().isNotFound
}
}

View File

@@ -0,0 +1,81 @@
package ltd.hlaeja.controller
import java.util.UUID
import ltd.hlaeja.library.deviceRegistry.Node
import ltd.hlaeja.test.container.PostgresContainer
import org.assertj.core.api.SoftAssertions
import org.assertj.core.api.junit.jupiter.InjectSoftAssertions
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody
@PostgresContainer
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ExtendWith(SoftAssertionsExtension::class)
class NodeEndpoint {
@InjectSoftAssertions
lateinit var softly: SoftAssertions
@LocalServerPort
var port: Int = 0
lateinit var webClient: WebTestClient
@BeforeEach
fun setup() {
webClient = WebTestClient.bindToServer().baseUrl("http://localhost:$port").build()
}
@Test
fun `added node - success`() {
// given
val name = "Node 4"
val device = UUID.fromString("00000000-0000-0000-0002-000000000001")
val client = UUID.fromString("00000000-0000-0000-0000-000000000000")
val request = Node.Request(device = device, client = client, name = name)
// when
val result = webClient.post().uri("/node").bodyValue(request).exchange()
// then
result.expectStatus()
.isCreated
.expectBody<Node.Response>()
.consumeWith {
softly.assertThat(it.responseBody?.id?.version()).isEqualTo(7)
softly.assertThat(it.responseBody?.device).isEqualTo(device)
softly.assertThat(it.responseBody?.client).isEqualTo(client)
softly.assertThat(it.responseBody?.name).isEqualTo(name)
}
}
@ParameterizedTest
@CsvSource(
// not a device
"'00000000-0000-0000-0002-000000000000'",
// already a node
"'00000000-0000-0000-0002-000000000002'",
)
fun `added node - fail`(device: String) {
// given
val name = "Node 5"
val client = UUID.fromString("00000000-0000-0000-0000-000000000000")
val request = Node.Request(device = UUID.fromString(device), client = client, name = name)
// when
val result = webClient.post().uri("/node").bodyValue(request).exchange()
// then
result.expectStatus()
.isBadRequest
}
}

View File

@@ -0,0 +1,291 @@
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
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.ACCEPTED
import org.springframework.http.HttpStatus.CONFLICT
import org.springframework.http.MediaType.APPLICATION_JSON
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody
@PostgresContainer
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ExtendWith(SoftAssertionsExtension::class)
class TypeEndpoint {
@InjectSoftAssertions
lateinit var softly: SoftAssertions
@LocalServerPort
var port: Int = 0
lateinit var webClient: WebTestClient
@BeforeEach
fun setup() {
webClient = WebTestClient.bindToServer().baseUrl("http://localhost:$port").build()
}
@Nested
inner class CreateType {
@Test
fun `added type - success`() {
// given
val name = "Thing 5 v1"
val description = "Thing 5 description"
val request = Type.Request(
name = name,
description = description,
)
// when
val result = webClient.post().uri("/type").bodyValue(request).exchange()
// then
result.expectStatus()
.isCreated
.expectBody<Type.Response>()
.consumeWith {
softly.assertThat(it.responseBody?.id?.version()).isEqualTo(7)
softly.assertThat(it.responseBody?.name).isEqualTo(name)
softly.assertThat(it.responseBody?.description).isEqualTo(description)
}
}
@Test
fun `added type - fail name take`() {
// given
val request = Type.Request(
name = "Thing 1 v1",
description = "Thing 1 description",
)
// when
val result = webClient.post().uri("/type").bodyValue(request).exchange()
// 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<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(ACCEPTED)
}
@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

@@ -0,0 +1,210 @@
package ltd.hlaeja.controller
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
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody
@PostgresContainer
@SpringBootTest(webEnvironment = RANDOM_PORT)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TypesEndpoint {
@LocalServerPort
var port: Int = 0
lateinit var webClient: WebTestClient
@BeforeEach
fun setup() {
webClient = WebTestClient.bindToServer().baseUrl("http://localhost:$port").build()
}
@Test
fun `get types default - success`() {
// when
val result = webClient.get().uri("/types").exchange()
// then
result.expectStatus().isOk()
.expectBody<List<Types.Response>>()
.consumeWith {
assertThat(it.responseBody?.size).isEqualTo(4)
}
}
@ParameterizedTest
@CsvSource(
value = [
"1,4",
"2,0",
],
)
fun `get types by page - success`(page: Int, expected: Int) {
// when
val result = webClient.get().uri("/types/page-$page").exchange()
// then
result.expectStatus().isOk()
.expectBody<List<Types.Response>>()
.consumeWith {
assertThat(it.responseBody?.size).isEqualTo(expected)
}
}
@Test
fun `get types by pages - fail`() {
// when
val result = webClient.get().uri("/types/page-0").exchange()
// then
result.expectStatus().isBadRequest
}
@ParameterizedTest
@CsvSource(
value = [
"1,2,2",
"2,2,2",
"3,2,0",
],
)
fun `get types by page and show - success`(page: Int, show: Int, expected: Int) {
// when
val result = webClient.get().uri("/types/page-$page/show-$show").exchange()
// then
result.expectStatus().isOk()
.expectBody<List<Types.Response>>()
.consumeWith {
assertThat(it.responseBody?.size).isEqualTo(expected)
}
}
@ParameterizedTest
@CsvSource(
value = [
"0,1",
"1,0",
],
)
fun `get types by page and show - fail`(page: Int, show: Int) {
// when
val result = webClient.get().uri("/types/page-$page/show-$show").exchange()
// then
result.expectStatus().isBadRequest
}
@ParameterizedTest
@CsvSource(
value = [
"'',4",
"v1,3",
"v2,1",
"v3,0",
],
)
fun `get types filter - success`(filter: String, expected: Int) {
// when
val result = webClient.get().uri("/types/filter-$filter").exchange()
// then
result.expectStatus().isOk()
.expectBody<List<Types.Response>>()
.consumeWith {
assertThat(it.responseBody?.size).isEqualTo(expected)
}
}
@ParameterizedTest
@CsvSource(
value = [
"'',1,4",
"'',2,0",
"v1,1,3",
"v1,2,0",
"v2,1,1",
"v2,2,0",
"v3,1,0",
],
)
fun `get types by filter and page - success`(filter: String, page: Int, expected: Int) {
// when
val result = webClient.get().uri("/types/filter-$filter/page-$page").exchange()
// then
result.expectStatus().isOk()
.expectBody<List<Types.Response>>()
.consumeWith {
assertThat(it.responseBody?.size).isEqualTo(expected)
}
}
@ParameterizedTest
@CsvSource(
value = [
"'',0",
"v1,0",
],
)
fun `get types by filter and page - fail`(filter: String, page: Int) {
// when
val result = webClient.get().uri("/types/filter-$filter/page-$page").exchange()
// then
result.expectStatus().isBadRequest
}
@ParameterizedTest
@CsvSource(
value = [
"'',1,2,2",
"'',2,2,2",
"'',3,2,0",
"v1,1,2,2",
"v1,2,2,1",
"v1,3,2,0",
"v2,1,2,1",
"v2,2,2,0",
"v3,1,2,0",
],
)
fun `get types by filter, page and show - success`(filter: String, page: Int, show: Int, expected: Int) {
// when
val result = webClient.get().uri("/types/filter-$filter/page-$page/show-$show").exchange()
// then
result.expectStatus().isOk()
.expectBody<List<Types.Response>>()
.consumeWith {
assertThat(it.responseBody?.size).isEqualTo(expected)
}
}
@ParameterizedTest
@CsvSource(
value = [
"'',1,0",
"'',0,1",
"v1,1,0",
"v1,0,1",
],
)
fun `get types by filter, page and show - fail`(filter: String, page: Int, show: Int) {
// when
val result = webClient.get().uri("/types/filter-$filter/page-$page/show-$show").exchange()
// then
result.expectStatus().isBadRequest
}
}

View File

@@ -6,3 +6,10 @@ spring:
url: r2dbc:postgresql://placeholder url: r2dbc:postgresql://placeholder
username: placeholder username: placeholder
password: placeholder password: placeholder
container:
postgres:
version: postgres:17
init: postgres/schema.sql
before: postgres/data.sql
after: postgres/reset.sql

View File

@@ -0,0 +1 @@
eyJhbGciOiJSUzI1NiJ9.eyJkZXZpY2UiOiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMi0wMDAwMDAwMDAwMDEifQ.JND8PsYw1fd_MfyMrxrufWfMrJka7cD5DVaLeNKuwXlmSWjYUm6NXj70ULTr2eOTiNSmDf-S2n_llfQx3ZkbEck9brpASzMgz-C7jUjxLB1jxEncqqjFbbM84ynt0btkLy4ZLvCDvQqrgNs1MHdz2DNg1OPrZx0kMp_RIeYvX3opM0PKPv5H0w_n-5iYuHx5SDcc0a_S_qHtU2zZSETNrdqe_i-6aCwFP6JO8OZvKVS2P_w7cF0uQUTpaCXF18VhfKeD1DB2OSG4L0HSS1aynXpZprmuKjFyFJIpFuD6zZKo1MNGBgIFufuWRc8iwsHrebWkyua5eACe36qL_vCVlg

View File

@@ -0,0 +1,23 @@
-- Test data
INSERT INTO public.types (id, timestamp, name)
VALUES ('00000000-0000-0000-0001-000000000001'::uuid, '2000-01-01 00:00:00.000001 +00:00', 'Thing 1 v1'),
('00000000-0000-0000-0001-000000000002'::uuid, '2000-01-01 00:00:00.000001 +00:00', 'Thing 2 v1'),
('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),
('00000000-0000-0000-0002-000000000003'::uuid, '2000-01-01 00:00:00.000001 +00:00', '00000000-0000-0000-0001-000000000002'::uuid),
('00000000-0000-0000-0002-000000000004'::uuid, '2000-01-01 00:00:00.000001 +00:00', '00000000-0000-0000-0001-000000000003'::uuid);
INSERT INTO public.nodes (id, timestamp, client, device, name)
VALUES ('00000000-0000-0000-0003-000000000001'::uuid, '2000-01-01 00:00:00.000001 +00:00', '00000000-0000-0000-0000-000000000000'::uuid, '00000000-0000-0000-0002-000000000002'::uuid, 'Node 1'),
('00000000-0000-0000-0003-000000000002'::uuid, '2000-01-01 00:00:00.000001 +00:00', '00000000-0000-0000-0000-000000000000'::uuid, '00000000-0000-0000-0002-000000000003'::uuid, 'Node 2'),
('00000000-0000-0000-0003-000000000003'::uuid, '2000-01-01 00:00:00.000001 +00:00', '00000000-0000-0000-0000-000000000000'::uuid, '00000000-0000-0000-0002-000000000004'::uuid, 'Node 3');

View File

@@ -0,0 +1,9 @@
-- Disable triggers on the tables
-- 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

View File

@@ -0,0 +1,78 @@
-- FUNCTION: public.gen_uuid_v7(timestamp with time zone)
CREATE OR REPLACE FUNCTION public.gen_uuid_v7(p_timestamp timestamp with time zone)
RETURNS uuid
LANGUAGE 'sql'
COST 100
VOLATILE PARALLEL UNSAFE
AS
$BODY$
-- Replace the first 48 bits of a uuid v4 with the provided timestamp (in milliseconds) since 1970-01-01 UTC, and set the version to 7
SELECT encode(set_bit(set_bit(overlay(uuid_send(gen_random_uuid()) PLACING substring(int8send((extract(EPOCH FROM p_timestamp) * 1000):: BIGINT) FROM 3) FROM 1 FOR 6), 52, 1), 53, 1), 'hex') ::uuid;
$BODY$;
-- FUNCTION: public.gen_uuid_v7()
CREATE OR REPLACE FUNCTION public.gen_uuid_v7()
RETURNS uuid
LANGUAGE 'sql'
COST 100
VOLATILE PARALLEL UNSAFE
AS
$BODY$
SELECT gen_uuid_v7(clock_timestamp());
$BODY$;
-- Table: public.types
CREATE TABLE IF NOT EXISTS public.types
(
id UUID DEFAULT gen_uuid_v7(),
timestamp timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
name VARCHAR(50) UNIQUE NOT NULL,
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
(
id UUID DEFAULT gen_uuid_v7(),
timestamp TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
type UUID NOT NULL,
CONSTRAINT pk_devices PRIMARY KEY (id),
CONSTRAINT fk_devices_type FOREIGN KEY (type) REFERENCES public.types (id) ON DELETE NO ACTION ON UPDATE NO ACTION
);
-- Index: public.i_devices_type
CREATE INDEX IF NOT EXISTS i_devices_type ON public.devices (type);
-- Table: public.nodes
CREATE TABLE IF NOT EXISTS public.nodes
(
id UUID DEFAULT gen_uuid_v7(),
timestamp TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
client UUID NOT NULL,
device UUID NOT NULL,
name VARCHAR(50) NOT NULL,
CONSTRAINT pk_nodes PRIMARY KEY (id),
CONSTRAINT fk_nodes_type FOREIGN KEY (device) REFERENCES public.devices (id) ON DELETE NO ACTION ON UPDATE NO ACTION
);
-- Index: public.i_nodes_type
CREATE UNIQUE INDEX IF NOT EXISTS i_nodes_device ON public.nodes (device);

View File

@@ -1,9 +1,9 @@
package ltd.hlaeja.controller package ltd.hlaeja.controller
import java.util.UUID import java.util.UUID
import ltd.hlaeja.jwt.service.PrivateJwtService
import ltd.hlaeja.library.deviceRegistry.Device import ltd.hlaeja.library.deviceRegistry.Device
import ltd.hlaeja.service.DeviceService import ltd.hlaeja.service.DeviceService
import ltd.hlaeja.service.JwtService
import ltd.hlaeja.util.toDeviceResponse import ltd.hlaeja.util.toDeviceResponse
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
@@ -14,18 +14,18 @@ import org.springframework.web.bind.annotation.RestController
@RestController @RestController
class DeviceController( class DeviceController(
private val deviceService: DeviceService, private val deviceService: DeviceService,
private val jwtService: JwtService, private val privateJwtService: PrivateJwtService,
) { ) {
@PostMapping("/device") @PostMapping("/device")
suspend fun addDevice( suspend fun addDevice(
@RequestBody request: Device.Request, @RequestBody request: Device.Request,
): Device.Response = deviceService.addDevice(request.type) ): Device.Response = deviceService.addDevice(request.type)
.toDeviceResponse(jwtService) .toDeviceResponse(privateJwtService)
@GetMapping("/device-{device}") @GetMapping("/device-{device}")
suspend fun getDevice( suspend fun getDevice(
@PathVariable device: UUID, @PathVariable device: UUID,
): Device.Response = deviceService.getDevice(device) ): Device.Response = deviceService.getDevice(device)
.toDeviceResponse(jwtService) .toDeviceResponse(privateJwtService)
} }

View File

@@ -4,8 +4,10 @@ import ltd.hlaeja.library.deviceRegistry.Node
import ltd.hlaeja.service.NodeService import ltd.hlaeja.service.NodeService
import ltd.hlaeja.util.toEntity import ltd.hlaeja.util.toEntity
import ltd.hlaeja.util.toNodeResponse import ltd.hlaeja.util.toNodeResponse
import org.springframework.http.HttpStatus.CREATED
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
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,6 +16,7 @@ class NodeController(
) { ) {
@PostMapping("/node") @PostMapping("/node")
@ResponseStatus(CREATED)
suspend fun addNode( suspend fun addNode(
@RequestBody request: Node.Request, @RequestBody request: Node.Request,
): Node.Response = nodeService.addNode(request.toEntity()).toNodeResponse() ): Node.Response = nodeService.addNode(request.toEntity()).toNodeResponse()

View File

@@ -1,14 +1,16 @@
package ltd.hlaeja.controller package ltd.hlaeja.controller
import kotlinx.coroutines.flow.Flow import java.util.UUID
import kotlinx.coroutines.flow.map
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.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
@@ -16,11 +18,20 @@ class TypeController(
private val service: TypeService, private val service: TypeService,
) { ) {
@GetMapping("/types")
fun getTypes(): Flow<Type.Response> = service.getTypes().map { it.toTypeResponse() }
@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

@@ -0,0 +1,36 @@
package ltd.hlaeja.controller
import jakarta.validation.constraints.Min
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import ltd.hlaeja.library.deviceRegistry.Types
import ltd.hlaeja.service.TypeService
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
@RestController
class TypesController(
private val service: TypeService,
) {
companion object {
const val DEFAULT_PAGE: Int = 1
const val DEFAULT_SIZE: Int = 25
}
@GetMapping(
"/types",
"/types/page-{page}",
"/types/page-{page}/show-{show}",
"/types/filter-{filter}",
"/types/filter-{filter}/page-{page}",
"/types/filter-{filter}/page-{page}/show-{show}",
)
fun getTypes(
@PathVariable(required = false) @Min(1) page: Int = DEFAULT_PAGE,
@PathVariable(required = false) @Min(1) show: Int = DEFAULT_SIZE,
@PathVariable(required = false) filter: String? = null,
): Flow<Types.Response> = service.getTypes((page - 1) * show, show, filter)
.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

@@ -1,9 +1,39 @@
package ltd.hlaeja.repository package ltd.hlaeja.repository
import java.util.UUID import java.util.UUID
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.repository.kotlin.CoroutineCrudRepository import org.springframework.data.repository.kotlin.CoroutineCrudRepository
import org.springframework.data.repository.query.Param
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
@Repository @Repository
interface TypeRepository : CoroutineCrudRepository<TypeEntity, UUID> interface TypeRepository : CoroutineCrudRepository<TypeEntity, UUID> {
@Query("SELECT * FROM types ORDER BY name LIMIT :limit OFFSET :offset")
fun findAll(
@Param("offset") offset: Int,
@Param("limit") limit: Int,
): Flow<TypeEntity>
@Query("SELECT * FROM types WHERE name ILIKE :filter ORDER BY name LIMIT :limit OFFSET :offset")
fun findAllContaining(
@Param("filter") filter: String,
@Param("offset") offset: Int,
@Param("limit") limit: Int,
): 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

@@ -5,6 +5,8 @@ import java.time.ZonedDateTime
import java.util.UUID import java.util.UUID
import ltd.hlaeja.entity.DeviceEntity import ltd.hlaeja.entity.DeviceEntity
import ltd.hlaeja.repository.DeviceRepository import ltd.hlaeja.repository.DeviceRepository
import org.springframework.dao.DataIntegrityViolationException
import org.springframework.http.HttpStatus.BAD_REQUEST
import org.springframework.http.HttpStatus.NOT_FOUND import org.springframework.http.HttpStatus.NOT_FOUND
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.web.server.ResponseStatusException import org.springframework.web.server.ResponseStatusException
@@ -18,8 +20,13 @@ class DeviceService(
suspend fun addDevice( suspend fun addDevice(
type: UUID, type: UUID,
): DeviceEntity = deviceRepository.save(DeviceEntity(null, ZonedDateTime.now(), type)) ): DeviceEntity = try {
.also { log.debug { "Added device ${it.id}" } } deviceRepository.save(DeviceEntity(null, ZonedDateTime.now(), type))
.also { log.debug { "Added device ${it.id}" } }
} catch (e: DataIntegrityViolationException) {
log.warn { e.localizedMessage }
throw ResponseStatusException(BAD_REQUEST)
}
suspend fun getDevice(device: UUID): DeviceEntity = deviceRepository.findById(device) suspend fun getDevice(device: UUID): DeviceEntity = deviceRepository.findById(device)
?.also { log.debug { "Get device ${it.id}" } } ?.also { log.debug { "Get device ${it.id}" } }

View File

@@ -1,25 +0,0 @@
package ltd.hlaeja.service
import io.jsonwebtoken.Jwts
import java.security.interfaces.RSAPrivateKey
import java.util.UUID
import ltd.hlaeja.property.JwtProperty
import ltd.hlaeja.util.PrivateKeyProvider
import org.springframework.stereotype.Service
@Service
class JwtService(
jwtProperty: JwtProperty,
) {
private var privateKey: RSAPrivateKey = PrivateKeyProvider.load(jwtProperty.privateKey)
suspend fun makeIdentity(device: UUID): String {
return Jwts.builder()
.claims()
.add("device", device)
.and()
.signWith(privateKey)
.compact()
}
}

View File

@@ -4,6 +4,8 @@ import io.github.oshai.kotlinlogging.KotlinLogging
import java.util.UUID import java.util.UUID
import ltd.hlaeja.entity.NodeEntity import ltd.hlaeja.entity.NodeEntity
import ltd.hlaeja.repository.NodeRepository import ltd.hlaeja.repository.NodeRepository
import org.springframework.dao.DataIntegrityViolationException
import org.springframework.http.HttpStatus.BAD_REQUEST
import org.springframework.http.HttpStatus.NOT_FOUND import org.springframework.http.HttpStatus.NOT_FOUND
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.web.server.ResponseStatusException import org.springframework.web.server.ResponseStatusException
@@ -17,8 +19,11 @@ class NodeService(
suspend fun addNode( suspend fun addNode(
node: NodeEntity, node: NodeEntity,
): NodeEntity = nodeRepository.save(node) ): NodeEntity = try {
.also { log.debug { "Added node ${it.id}" } } nodeRepository.save(node).also { log.debug { "Added node ${it.id}" } }
} catch (exception: DataIntegrityViolationException) {
throw ResponseStatusException(BAD_REQUEST, null, exception)
}
suspend fun getNodeFromDevice( suspend fun getNodeFromDevice(
device: UUID, device: UUID,

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.ACCEPTED
import org.springframework.http.HttpStatus.CONFLICT
import org.springframework.http.HttpStatus.EXPECTATION_FAILED
import org.springframework.http.HttpStatus.NOT_FOUND
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,17 +23,99 @@ 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(): Flow<TypeEntity> = typeRepository.findAll() fun getTypes(
page: Int,
show: Int,
filter: String?,
): Flow<TypeEntity> = when {
!filter.isNullOrEmpty() -> typeRepository.findAllContaining("%$filter%", 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(ACCEPTED, "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.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.service.JwtService 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(
@@ -40,10 +61,10 @@ fun NodeEntity.toIdentityResponse(): Identity.Response = Identity.Response(
device, device,
) )
suspend fun DeviceEntity.toDeviceResponse( fun DeviceEntity.toDeviceResponse(
jwtService: JwtService, jwtService: PrivateJwtService,
): Device.Response = Device.Response( ): Device.Response = Device.Response(
id ?: throw ResponseStatusException(EXPECTATION_FAILED), id ?: throw ResponseStatusException(EXPECTATION_FAILED),
type, type,
jwtService.makeIdentity(id), jwtService.sign("device" to id),
) )

View File

@@ -1,35 +0,0 @@
package ltd.hlaeja.util
import java.security.KeyFactory
import java.security.interfaces.RSAPrivateKey
import java.security.spec.PKCS8EncodedKeySpec
import java.util.Base64.getDecoder
import ltd.hlaeja.exception.KeyProviderException
object PrivateKeyProvider {
fun load(
pemFile: String,
): RSAPrivateKey = readPrivatePemFile(pemFile)
.let(::makePrivateKey)
private fun makePrivateKey(
privateKeyBytes: ByteArray,
): RSAPrivateKey = KeyFactory.getInstance("RSA")
.generatePrivate(PKCS8EncodedKeySpec(privateKeyBytes)) as RSAPrivateKey
private fun readPrivatePemFile(
privateKey: String,
): ByteArray = javaClass.classLoader
.getResource(privateKey)
?.readText()
?.let(::getPrivateKeyByteArray)
?: throw KeyProviderException("Could not load private key")
private fun getPrivateKeyByteArray(
keyText: String,
): ByteArray = keyText.replace(Regex("[\r\n]+"), "")
.removePrefix("-----BEGIN PRIVATE KEY-----")
.removeSuffix("-----END PRIVATE KEY-----")
.let { getDecoder().decode(it) }
}

View File

@@ -19,11 +19,6 @@
"name": "spring.application.build.os.version", "name": "spring.application.build.os.version",
"type": "java.lang.String", "type": "java.lang.String",
"description": "Application build os version." "description": "Application build os version."
},
{
"name": "jwt.private-key",
"type": "java.lang.String",
"description": "Jwt private key file."
} }
] ]
} }

View File

@@ -1,13 +0,0 @@
package ltd.hlaeja
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
class ApplicationTests {
@Test
fun contextLoads() {
// place holder
}
}

View File

@@ -1,15 +0,0 @@
package ltd.hlaeja.assertj
import java.util.UUID
import org.assertj.core.api.AbstractAssert
class UUIDAssert(actual: UUID) : AbstractAssert<UUIDAssert, UUID>(actual, UUIDAssert::class.java) {
fun isUUID(expected: String): UUIDAssert {
objects.assertEqual(this.info, this.actual, UUID.fromString(expected))
return this.myself
}
}
fun assertThat(actual: UUID): UUIDAssert {
return UUIDAssert(actual)
}

View File

@@ -9,9 +9,9 @@ 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.DeviceEntity import ltd.hlaeja.entity.DeviceEntity
import ltd.hlaeja.jwt.service.PrivateJwtService
import ltd.hlaeja.library.deviceRegistry.Device import ltd.hlaeja.library.deviceRegistry.Device
import ltd.hlaeja.service.DeviceService import ltd.hlaeja.service.DeviceService
import ltd.hlaeja.service.JwtService
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
import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Nested
@@ -28,13 +28,13 @@ class DeviceControllerTest {
} }
val deviceService: DeviceService = mockk() val deviceService: DeviceService = mockk()
val jwtService: JwtService = mockk() val privateJwtService: PrivateJwtService = mockk()
lateinit var controller: DeviceController lateinit var controller: DeviceController
@BeforeEach @BeforeEach
fun setUp() { fun setUp() {
controller = DeviceController(deviceService, jwtService) controller = DeviceController(deviceService, privateJwtService)
} }
@Nested @Nested
@@ -45,14 +45,14 @@ class DeviceControllerTest {
// given // given
val request = Device.Request(uuid) val request = Device.Request(uuid)
coEvery { deviceService.addDevice(any()) } returns DeviceEntity(uuid, timestamp, uuid) coEvery { deviceService.addDevice(any()) } returns DeviceEntity(uuid, timestamp, uuid)
coEvery { jwtService.makeIdentity(any()) } returns PAYLOAD coEvery { privateJwtService.sign(any()) } returns PAYLOAD
// when // when
val response = controller.addDevice(request) val response = controller.addDevice(request)
// then // then
coVerify(exactly = 1) { deviceService.addDevice(any()) } coVerify(exactly = 1) { deviceService.addDevice(any()) }
coVerify(exactly = 1) { jwtService.makeIdentity(any()) } coVerify(exactly = 1) { privateJwtService.sign(any()) }
assertThat(response.identity).isEqualTo(PAYLOAD) assertThat(response.identity).isEqualTo(PAYLOAD)
} }
@@ -80,14 +80,14 @@ class DeviceControllerTest {
fun `get device - success`() = runTest { fun `get device - success`() = runTest {
// given // given
coEvery { deviceService.getDevice(any()) } returns DeviceEntity(uuid, timestamp, uuid) coEvery { deviceService.getDevice(any()) } returns DeviceEntity(uuid, timestamp, uuid)
coEvery { jwtService.makeIdentity(any()) } returns PAYLOAD coEvery { privateJwtService.sign(any()) } returns PAYLOAD
// when // when
val response = controller.getDevice(uuid) val response = controller.getDevice(uuid)
// then // then
coVerify(exactly = 1) { deviceService.getDevice(any()) } coVerify(exactly = 1) { deviceService.getDevice(any()) }
coVerify(exactly = 1) { jwtService.makeIdentity(any()) } coVerify(exactly = 1) { privateJwtService.sign(any()) }
assertThat(response.identity).isEqualTo(PAYLOAD) assertThat(response.identity).isEqualTo(PAYLOAD)
} }

View File

@@ -8,9 +8,10 @@ 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.assertj.assertThat
import ltd.hlaeja.entity.NodeEntity import ltd.hlaeja.entity.NodeEntity
import ltd.hlaeja.service.NodeService import ltd.hlaeja.service.NodeService
import ltd.hlaeja.test.isEqualToUuid
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@@ -48,8 +49,8 @@ class IdentityControllerTest {
// then // then
coVerify(exactly = 1) { service.getNodeFromDevice(any()) } coVerify(exactly = 1) { service.getNodeFromDevice(any()) }
assertThat(response.node).isUUID("00000000-0000-0000-0000-000000000001") assertThat(response.node).isEqualToUuid("00000000-0000-0000-0000-000000000001")
assertThat(response.client).isUUID("00000000-0000-0000-0000-000000000002") assertThat(response.client).isEqualToUuid("00000000-0000-0000-0000-000000000002")
assertThat(response.device).isUUID("00000000-0000-0000-0000-000000000003") assertThat(response.device).isEqualToUuid("00000000-0000-0000-0000-000000000003")
} }
} }

View File

@@ -5,10 +5,10 @@ import io.mockk.coVerify
import io.mockk.mockk import io.mockk.mockk
import java.util.UUID import java.util.UUID
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import ltd.hlaeja.assertj.assertThat
import ltd.hlaeja.entity.NodeEntity import ltd.hlaeja.entity.NodeEntity
import ltd.hlaeja.library.deviceRegistry.Node import ltd.hlaeja.library.deviceRegistry.Node
import ltd.hlaeja.service.NodeService import ltd.hlaeja.service.NodeService
import ltd.hlaeja.test.isEqualToUuid
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
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@@ -42,9 +42,9 @@ class NodeControllerTest {
// then // then
coVerify(exactly = 1) { service.addNode(any()) } coVerify(exactly = 1) { service.addNode(any()) }
assertThat(response.id).isUUID("00000000-0000-0000-0000-000000000003") assertThat(response.id).isEqualToUuid("00000000-0000-0000-0000-000000000003")
assertThat(response.client).isUUID("00000000-0000-0000-0000-000000000001") assertThat(response.client).isEqualToUuid("00000000-0000-0000-0000-000000000001")
assertThat(response.device).isUUID("00000000-0000-0000-0000-000000000002") assertThat(response.device).isEqualToUuid("00000000-0000-0000-0000-000000000002")
assertThat(response.name).isEqualTo("test") assertThat(response.name).isEqualTo("test")
} }
} }

View File

@@ -2,20 +2,16 @@ package ltd.hlaeja.controller
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.verify
import java.time.LocalDateTime 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 kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.single
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.assertj.assertThat import ltd.hlaeja.test.isEqualToUuid
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
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@@ -36,33 +32,61 @@ class TypeControllerTest {
} }
@Test @Test
fun `get all types`() = runTest { fun `add type`() = runTest {
// given // given
every { service.getTypes() } returns flowOf(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.getTypes().single()
// then
verify(exactly = 1) { service.getTypes() }
assertThat(response.id).isUUID("00000000-0000-0000-0000-000000000000")
assertThat(response.name).isEqualTo("name")
}
@Test
fun `add types`() = runTest {
// given
val request = Type.Request("name")
coEvery { service.addType(any()) } returns TypeEntity(id, timestamp, "name")
// 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).isUUID("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

@@ -0,0 +1,54 @@
package ltd.hlaeja.controller
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
import java.util.UUID
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.single
import kotlinx.coroutines.test.runTest
import ltd.hlaeja.entity.TypeEntity
import ltd.hlaeja.service.TypeService
import ltd.hlaeja.test.isEqualToUuid
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class TypesControllerTest {
companion object {
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()
lateinit var controller: TypesController
@BeforeEach
fun setUp() {
controller = TypesController(service)
}
@Test
fun `get all types`() = runTest {
// given
every {
service.getTypes(any(), any(), any())
} returns flowOf(TypeEntity(id, timestamp, NAME))
// when
val response = controller.getTypes().single()
// then
verify(exactly = 1) { service.getTypes(0, 25, null) }
assertThat(response.id).isEqualToUuid(NIL_UUID)
assertThat(response.name).isEqualTo(NAME)
assertThat(response.timestamp).isEqualTo(timestamp)
}
}

View File

@@ -1,31 +0,0 @@
package ltd.hlaeja.service
import java.util.UUID
import kotlinx.coroutines.test.runTest
import ltd.hlaeja.property.JwtProperty
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class JwtServiceTest {
val property: JwtProperty = JwtProperty("cert/valid-private-key.pem")
lateinit var service: JwtService
@BeforeEach
fun setUp() {
service = JwtService(property)
}
@Test
fun `should generate a JWT successfully with a valid private key`() = runTest {
// given
val deviceId = UUID.fromString("00000000-0000-0000-0000-000000000000")
// when
val jwt = service.makeIdentity(deviceId)
// then
assertThat(jwt).contains("eyJkZXZpY2UiOiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAifQ")
}
}

View File

@@ -4,76 +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.ACCEPTED
import org.springframework.http.HttpStatus.CONFLICT
import org.springframework.http.HttpStatus.EXPECTATION_FAILED
import org.springframework.http.HttpStatus.NOT_FOUND
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() } returns flowOf(mockk<TypeEntity>()) every { typeRepository.findAll(any(), any()) } returns flowOf(mockk<TypeEntity>())
// when // when
service.getTypes() service.getTypes(1, 10, null)
// then // then
verify(exactly = 1) { repository.findAll() } verify(exactly = 1) { typeRepository.findAll(1, 10) }
verify(exactly = 0) { typeRepository.findAllContaining(any(), any(), any()) }
}
@Test
fun `get all types with filter`() {
// given
every { typeRepository.findAllContaining(any(), any(), any()) } returns flowOf(mockk<TypeEntity>())
// when
service.getTypes(1, 10, "abc")
// then
verify(exactly = 1) { typeRepository.findAllContaining("%abc%", 1, 10) }
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,
coEvery { repository.save(any()) } answers { call -> (call.invocation.args[0] as TypeEntity).copy(id = uuid) } description = call.invocation.args[1] as String,
)
}
// 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> {
// then exception service.addType("name", "description")
assertFailsWith<ResponseStatusException> {
service.addType(entity)
} }
// 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<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(ACCEPTED)
}
@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,11 +9,14 @@ 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.assertj.assertThat 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.library.deviceRegistry.Type 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.test.isEqualToUuid
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Assertions.assertThrows
@@ -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).isUUID("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")
@@ -119,9 +175,9 @@ class MappingKtTest {
val result = entity.toNodeResponse() val result = entity.toNodeResponse()
// then // then
assertThat(result.id).isUUID("00000000-0000-0000-0000-000000000001") assertThat(result.id).isEqualToUuid("00000000-0000-0000-0000-000000000001")
assertThat(result.client).isUUID("00000000-0000-0000-0000-000000000002") assertThat(result.client).isEqualToUuid("00000000-0000-0000-0000-000000000002")
assertThat(result.device).isUUID("00000000-0000-0000-0000-000000000003") assertThat(result.device).isEqualToUuid("00000000-0000-0000-0000-000000000003")
assertThat(result.name).isEqualTo("test") assertThat(result.name).isEqualTo("test")
} }
@@ -164,9 +220,9 @@ class MappingKtTest {
val result = entity.toIdentityResponse() val result = entity.toIdentityResponse()
// then // then
assertThat(result.node).isUUID("00000000-0000-0000-0000-000000000001") assertThat(result.node).isEqualToUuid("00000000-0000-0000-0000-000000000001")
assertThat(result.client).isUUID("00000000-0000-0000-0000-000000000002") assertThat(result.client).isEqualToUuid("00000000-0000-0000-0000-000000000002")
assertThat(result.device).isUUID("00000000-0000-0000-0000-000000000003") assertThat(result.device).isEqualToUuid("00000000-0000-0000-0000-000000000003")
} }
@Test @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")
}
}
} }

View File

@@ -1,51 +0,0 @@
package ltd.hlaeja.util
import java.security.interfaces.RSAPrivateKey
import ltd.hlaeja.exception.KeyProviderException
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
class PrivateKeyProviderTest {
@Test
fun `load private key - success`() {
// given
val pemFilePath = "cert/valid-private-key.pem"
// when
val privateKey: RSAPrivateKey = PrivateKeyProvider.load(pemFilePath)
// then
assertThat(privateKey).isNotNull
assertThat(privateKey.algorithm).isEqualTo("RSA")
}
@Test
fun `load private key - file does not exist`() {
// given
val nonExistentPemFilePath = "cert/non-existent.pem"
// when exception
val exception = assertThrows<KeyProviderException> {
PrivateKeyProvider.load(nonExistentPemFilePath)
}
// then
assertThat(exception.message).isEqualTo("Could not load private key")
}
@Test
fun `load private key - file is invalid`() {
// given
val invalidPemFilePath = "cert/invalid-private-key.pem"
// when exception
val exception = assertThrows<IllegalArgumentException> {
PrivateKeyProvider.load(invalidPemFilePath)
}
// then
assertThat(exception.message).contains("Input byte array has wrong 4-byte ending unit")
}
}

View File

@@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
VEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBK
VU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMg
SVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBU
SElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpV
TksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJ
UyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRI
SVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVO
SyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElT
IEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJ
UyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5L
IFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMg
SlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElT
IElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksg
VEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBK
VU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMg
SVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBU
SElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpV
TksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJ
UyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRI
SVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVO
SyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElT
IEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJ
UyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5L
IFRISVMgSVMgSlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMg
SlVOSyBUSElTIElTIEpVTksgVEhJUyBJUyBKVU5LIFRISVMgSVMgSlVOSyBUSElT
IElTIEpVTksg==
-----END PRIVATE KEY-----