20 Commits

Author SHA1 Message Date
hlaeja
f3f10dcfc8 [RELEASE] - Release version: 0.7.0 2025-08-18 11:00:17 +00:00
119d14eb46 add nodes endpoint
- add NodesController
  - add NodesEndpoint
  - add NodesControllerTest
  - add NodesController
  - add nodes.http

- add NodeEntity toNodesResponse in Mapping.kt

- add getNodes to NodeService

- add findAll to NodeRepository
2025-08-18 12:59:14 +02:00
19aa9c8b6b Extract default page and size 2025-08-18 12:59:14 +02:00
14d36f21db add devices by type endpoint
- update DevicesController

  - update DevicesEndpoint
  - update DevicesControllerTest
  - add getDevicesByType to DevicesController
  - update devices.http

- add getDevicesByType to DeviceService

- add findAllByType to DeviceRepository
2025-08-18 12:59:14 +02:00
6f44c05330 add devices endpoint
- fix missing coroutine in
  - TypeRepository
  - TypesController
  - TypeService
  - TypesControllerTest
  - TypeServiceTest
- add DevicesEndpoint
- add DevicesControllerTest
- add devices.http
- add DevicesController
- add DeviceEntity.toDevicesResponse() to Mapping.kt
- add PostgresTestContainer to DeviceService
- update DeviceRepository with find all
- update version catalog
  - update container annotation in DeviceEndpoint
  - update container annotation in IdentityEndpoint
  - update container annotation in NodeEndpoint
  - update container annotation in TypeEndpoint
  - update container annotation in TypesEndpoint
  - update version in gradle.properties
2025-08-18 12:59:14 +02:00
hlaeja
fc95f5d4b8 [RELEASE] - Bump version 2025-07-29 18:14:37 +00:00
hlaeja
83bce6d873 [RELEASE] - Release version: 0.6.0 2025-07-29 18:14:35 +00:00
4393d160e1 update gradlew 2025-07-29 20:13:59 +02:00
5d735ccfbe update project 2025-07-29 20:13:59 +02:00
cf8ca86c32 add GitHub action
- update release in README.md
- add action run checks
- add action release
- remove release.sh
2025-07-29 20:13:59 +02:00
58848e5b54 add actuator.http 2025-07-29 20:13:59 +02:00
59dd6286fe update sql files 2025-07-29 20:13:59 +02:00
9e9dc0fef6 [RELEASE] - bump version 2025-04-05 12:55:27 +02:00
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
69 changed files with 2440 additions and 267 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

12
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
name: Release
on:
workflow_dispatch:
jobs:
release:
uses: swordsteel/hlaeja-common-workflows/.github/workflows/release.yml@master
secrets:
CI_BOT_PAT: ${{ secrets.CI_BOT_PAT }}
with:
TYPE: service

12
.github/workflows/run-checks.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
name: Pull request validation
on:
pull_request:
paths-ignore:
- '.github/**'
jobs:
validate:
uses: swordsteel/hlaeja-common-workflows/.github/workflows/run-checks.yml@master
secrets:
CI_BOT_PAT: ${{ secrets.CI_BOT_PAT }}

View File

@@ -16,7 +16,7 @@ Classes crafted, identities bestowed, Each device recorded, their functions unfo
## Releasing Service ## Releasing Service
Run `release.sh` script from `master` branch. Run release pipeline from `master` branch.
## Development Information ## Development Information

View File

@@ -1,10 +1,10 @@
plugins { plugins {
alias(hlaeja.plugins.kotlin.jvm) alias(hlaeja.plugins.kotlin.jvm)
alias(hlaeja.plugins.kotlin.spring) alias(hlaeja.plugins.kotlin.spring)
alias(hlaeja.plugins.ltd.hlaeja.plugin.certificate) alias(hlaeja.plugins.spring.boot)
alias(hlaeja.plugins.ltd.hlaeja.plugin.service)
alias(hlaeja.plugins.spring.dependency.management) alias(hlaeja.plugins.spring.dependency.management)
alias(hlaeja.plugins.springframework.boot) alias(hlaeja.plugins.certificate)
alias(hlaeja.plugins.service)
} }
dependencies { dependencies {
@@ -12,23 +12,33 @@ dependencies {
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.hlaeja.jwt) 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.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)
testIntegrationImplementation(hlaeja.assertj.core)
testIntegrationImplementation(hlaeja.library.test)
testIntegrationImplementation(hlaeja.projectreactor.reactor.test)
testIntegrationImplementation(hlaeja.kotlin.test.junit5)
testIntegrationImplementation(hlaeja.kotlinx.coroutines.test)
testIntegrationImplementation(hlaeja.springboot.starter.test)
testIntegrationRuntimeOnly(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.4.0 version=0.7.0
catalog=0.8.0 catalog=0.12.0
container.port.host=9010 container.port.host=9010

Binary file not shown.

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

9
gradlew vendored
View File

@@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@@ -115,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;; NONSTOP* ) nonstop=true ;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
@@ -206,7 +205,7 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command: # Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped. # and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line. # treated as '${Hostname}' itself on the command line.
@@ -214,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \ -classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@" "$@"
# Stop when "xargs" is not available. # Stop when "xargs" is not available.

4
gradlew.bat vendored
View File

@@ -70,11 +70,11 @@ goto fail
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar set CLASSPATH=
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell

5
http/actuator.http Normal file
View File

@@ -0,0 +1,5 @@
### get actuator
GET {{hostname}}/actuator
### get actuator health
GET {{hostname}}/actuator/health

17
http/devices.http Normal file
View File

@@ -0,0 +1,17 @@
### get all types
GET {{hostname}}/devices
### get all types
GET {{hostname}}/devices/page-1
### get all types
GET {{hostname}}/devices/page-1/show-2
### get all types
GET {{hostname}}/devices/type-00000000-0000-0000-0000-000000000000
### get all types
GET {{hostname}}/devices/type-00000000-0000-0000-0000-000000000000/page-1
### get all types
GET {{hostname}}/devices/type-00000000-0000-0000-0000-000000000000/page-1/show-2

8
http/nodes.http Normal file
View File

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

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

@@ -1,89 +0,0 @@
#!/bin/sh
### This should be a pipeline, but for this example let use this ###
check_active_branch() {
if [ "$(git rev-parse --abbrev-ref HEAD)" != "$1" ]; then
echo "Error: The current branch is not $1."
exit 1
fi
}
check_uncommitted_changes() {
if [ -n "$(git status --porcelain)" ]; then
echo "Error: There are uncommitted changes in the repository."
exit 1
fi
}
prepare_environment() {
git fetch origin
}
check_last_commit() {
last_commit_message=$(git log -1 --pretty=format:%s)
if [ "$last_commit_message" = "[RELEASE] - bump version" ]; then
echo "Warning: Nothing to release!!!"
exit 1
fi
}
check_differences() {
if ! git diff --quiet origin/"$1" "$1"; then
echo "Error: The branches origin/$1 and $1 have differences."
exit 1
fi
}
un_snapshot_version() {
sed -i "s/\($1\s*=\s*[0-9]\+\.[0-9]\+\.[0-9]\+\).*/\1/" gradle.properties
}
current_version() {
awk -F '=' '/version\s*=\s*[0-9.]*/ {gsub(/^ +| +$/,"",$2); print $2}' gradle.properties
}
stage_files() {
for file in "$@"; do
if git diff --exit-code --quiet -- "$file"; then
echo "No changes in $file"
else
git add "$file"
echo "Changes in $file staged for commit"
fi
done
}
commit_change() {
stage_files gradle.properties
git commit -m "[RELEASE] - $1"
git push --porcelain origin master
}
add_release_tag() {
gitTag="v$(current_version)"
git tag -a "$gitTag" -m "Release version $gitTag"
git push --porcelain origin "$gitTag"
}
snapshot_version() {
new_version="$(current_version | awk -F '.' '{print $1 "." $2+1 ".0"}')"
sed -i "s/\(version\s*=\s*\)[0-9.]*/\1$new_version-SNAPSHOT/" gradle.properties
}
# check and prepare for release
check_active_branch master
check_uncommitted_changes
prepare_environment
check_last_commit
check_differences master
# un-snapshot version for release
un_snapshot_version version
un_snapshot_version catalog
# release changes and prepare for next release
commit_change "release version: $(current_version)"
add_release_tag
snapshot_version
commit_change 'bump version'

View File

@@ -56,10 +56,12 @@ CREATE DATABASE device_registry
WITH WITH
OWNER = role_administrator OWNER = role_administrator
ENCODING = 'UTF8' ENCODING = 'UTF8'
LC_COLLATE = 'en_US.utf8' LC_COLLATE = 'en_US.UTF-8'
LC_CTYPE = 'en_US.utf8' LC_CTYPE = 'en_US.UTF-8'
LOCALE_PROVIDER = 'libc' LOCALE_PROVIDER = 'libc'
TABLESPACE = pg_default TABLESPACE = pg_default
CONNECTION LIMIT = -1 CONNECTION LIMIT = -1
IS_TEMPLATE = False; IS_TEMPLATE = False;
COMMENT ON DATABASE device_registry
IS 'Database for managing device types, registered devices, and their deployment as nodes.';

View File

@@ -3,15 +3,20 @@
CREATE TABLE IF NOT EXISTS public.types CREATE TABLE IF NOT EXISTS public.types
( (
id UUID DEFAULT gen_uuid_v7(), id UUID DEFAULT gen_uuid_v7(),
timestamp timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP, timestamp timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
name VARCHAR(50) UNIQUE NOT NULL, name VARCHAR(50) NOT NULL,
CONSTRAINT pk_contact_types PRIMARY KEY (id) CONSTRAINT pk_contact_types PRIMARY KEY (id)
); );
ALTER TABLE IF EXISTS public.types ALTER TABLE IF EXISTS public.types
OWNER to role_administrator; 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 permissions from existing roles
REVOKE ALL ON TABLE public.types FROM role_administrator, role_maintainer, role_support, role_service; REVOKE ALL ON TABLE public.types FROM role_administrator, role_maintainer, role_support, role_service;

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;

5
sql/v0.5.0/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);

6
sql/v0.5.0/002-types.sql Normal file
View File

@@ -0,0 +1,6 @@
-- make name index unique order by name
DROP INDEX IF EXISTS types_name_key;
CREATE UNIQUE INDEX IF NOT EXISTS types_name_key
ON types (name ASC);

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,43 @@
package ltd.hlaeja.controller
import jakarta.validation.constraints.Min
import java.util.UUID
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import ltd.hlaeja.library.deviceRegistry.Devices
import ltd.hlaeja.service.DeviceService
import ltd.hlaeja.util.Pagination.DEFAULT_PAGE
import ltd.hlaeja.util.Pagination.DEFAULT_SIZE
import ltd.hlaeja.util.toDevicesResponse
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RestController
@RestController
class DevicesController(
private val deviceService: DeviceService,
) {
@GetMapping(
"/devices",
"/devices/page-{page}",
"/devices/page-{page}/show-{show}",
)
suspend fun getDevices(
@PathVariable(required = false) @Min(1) page: Int = DEFAULT_PAGE,
@PathVariable(required = false) @Min(1) show: Int = DEFAULT_SIZE,
): Flow<Devices.Response> = deviceService.getDevices((page - 1) * show, show)
.map { it.toDevicesResponse() }
@GetMapping(
"/devices/type-{type}",
"/devices/type-{type}/page-{page}",
"/devices/type-{type}/page-{page}/show-{show}",
)
suspend fun getDevicesByType(
@PathVariable type: UUID,
@PathVariable(required = false) @Min(1) page: Int = DEFAULT_PAGE,
@PathVariable(required = false) @Min(1) show: Int = DEFAULT_SIZE,
): Flow<Devices.Response> = deviceService.getDevicesByType(type, (page - 1) * show, show)
.map { it.toDevicesResponse() }
}

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

@@ -0,0 +1,30 @@
package ltd.hlaeja.controller
import jakarta.validation.constraints.Min
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import ltd.hlaeja.library.deviceRegistry.Nodes
import ltd.hlaeja.service.NodeService
import ltd.hlaeja.util.Pagination.DEFAULT_PAGE
import ltd.hlaeja.util.Pagination.DEFAULT_SIZE
import ltd.hlaeja.util.toNodesResponse
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RestController
@RestController
class NodesController(
private val service: NodeService,
) {
@GetMapping(
"/nodes",
"/nodes/page-{page}",
"/nodes/page-{page}/show-{show}",
)
suspend fun getNodes(
@PathVariable(required = false) @Min(1) page: Int = DEFAULT_PAGE,
@PathVariable(required = false) @Min(1) show: Int = DEFAULT_SIZE,
): Flow<Nodes.Response> = service.getNodes((page - 1) * show, show)
.map { it.toNodesResponse() }
}

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,34 @@
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.Pagination.DEFAULT_PAGE
import ltd.hlaeja.util.Pagination.DEFAULT_SIZE
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,
) {
@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}",
)
suspend fun getTypes(
@PathVariable(required = false) @Min(1) page: Int = DEFAULT_PAGE,
@PathVariable(required = false) @Min(1) show: Int = DEFAULT_SIZE,
@PathVariable(required = false) filter: String? = null,
): 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

@@ -1,9 +1,26 @@
package ltd.hlaeja.repository package ltd.hlaeja.repository
import java.util.UUID import java.util.UUID
import kotlinx.coroutines.flow.Flow
import ltd.hlaeja.entity.DeviceEntity import ltd.hlaeja.entity.DeviceEntity
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 DeviceRepository : CoroutineCrudRepository<DeviceEntity, UUID> interface DeviceRepository : CoroutineCrudRepository<DeviceEntity, UUID> {
@Query("SELECT * FROM devices LIMIT :limit OFFSET :offset")
fun findAll(
@Param("offset") offset: Int,
@Param("limit") limit: Int,
): Flow<DeviceEntity>
@Query("SELECT * FROM devices WHERE type = :type LIMIT :limit OFFSET :offset")
fun findAllByType(
@Param("type") type: UUID,
@Param("offset") offset: Int,
@Param("limit") limit: Int,
): Flow<DeviceEntity>
}

View File

@@ -1,9 +1,11 @@
package ltd.hlaeja.repository package ltd.hlaeja.repository
import java.util.UUID import java.util.UUID
import kotlinx.coroutines.flow.Flow
import ltd.hlaeja.entity.NodeEntity import ltd.hlaeja.entity.NodeEntity
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.stereotype.Repository import org.springframework.stereotype.Repository
@Repository @Repository
@@ -11,4 +13,10 @@ interface NodeRepository : CoroutineCrudRepository<NodeEntity, UUID> {
@Query("SELECT * FROM nodes WHERE device = :device") @Query("SELECT * FROM nodes WHERE device = :device")
suspend fun findByDevice(device: UUID): NodeEntity? suspend fun findByDevice(device: UUID): NodeEntity?
@Query("SELECT * FROM nodes LIMIT :limit OFFSET :offset")
suspend fun findAll(
@Param("offset") offset: Int,
@Param("limit") limit: Int,
): Flow<NodeEntity>
} }

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")
suspend 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")
suspend 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

@@ -3,6 +3,7 @@ package ltd.hlaeja.service
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.util.UUID import java.util.UUID
import kotlinx.coroutines.flow.Flow
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.dao.DataIntegrityViolationException
@@ -31,4 +32,15 @@ class DeviceService(
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}" } }
?: throw ResponseStatusException(NOT_FOUND) ?: throw ResponseStatusException(NOT_FOUND)
suspend fun getDevices(
page: Int,
show: Int,
): Flow<DeviceEntity> = deviceRepository.findAll(page, show)
suspend fun getDevicesByType(
type: UUID,
page: Int,
show: Int,
): Flow<DeviceEntity> = deviceRepository.findAllByType(type, page, show)
} }

View File

@@ -2,8 +2,11 @@ package ltd.hlaeja.service
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import java.util.UUID import java.util.UUID
import kotlinx.coroutines.flow.Flow
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,11 +20,19 @@ 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,
): NodeEntity = nodeRepository.findByDevice(device) ): NodeEntity = nodeRepository.findByDevice(device)
?: throw ResponseStatusException(NOT_FOUND) ?: throw ResponseStatusException(NOT_FOUND)
suspend fun getNodes(
page: Int,
show: Int,
): Flow<NodeEntity> = nodeRepository.findAll(page, show)
} }

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() suspend 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,45 @@
package ltd.hlaeja.util package ltd.hlaeja.util
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.util.UUID
import ltd.hlaeja.dto.TypeWithDescription
import ltd.hlaeja.entity.DeviceEntity import ltd.hlaeja.entity.DeviceEntity
import ltd.hlaeja.entity.NodeEntity import ltd.hlaeja.entity.NodeEntity
import ltd.hlaeja.entity.TypeDescriptionEntity
import ltd.hlaeja.entity.TypeEntity import ltd.hlaeja.entity.TypeEntity
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.Devices
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.Nodes
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(
@@ -47,3 +70,17 @@ fun DeviceEntity.toDeviceResponse(
type, type,
jwtService.sign("device" to id), jwtService.sign("device" to id),
) )
fun DeviceEntity.toDevicesResponse(): Devices.Response = Devices.Response(
id ?: throw ResponseStatusException(EXPECTATION_FAILED),
type,
timestamp,
)
fun NodeEntity.toNodesResponse(): Nodes.Response = Nodes.Response(
id ?: throw ResponseStatusException(EXPECTATION_FAILED),
timestamp,
client,
device,
name,
)

View File

@@ -0,0 +1,6 @@
package ltd.hlaeja.util
object Pagination {
const val DEFAULT_PAGE: Int = 1
const val DEFAULT_SIZE: Int = 25
}

View File

@@ -10,6 +10,20 @@ spring:
name: "%APP_BUILD_OS_NAME%" name: "%APP_BUILD_OS_NAME%"
version: "%APP_BUILD_OS_VERSION%" version: "%APP_BUILD_OS_VERSION%"
management:
endpoints:
access:
default: none
web:
exposure:
include: "health,info"
endpoint:
health:
show-details: always
access: read_only
info:
access: read_only
jwt: jwt:
private-key: cert/private_key.pem private-key: cert/private_key.pem

View File

@@ -1,11 +0,0 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
<logger level="DEBUG" name="ltd.hlaeja"/>
</configuration>

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.PostgresTestContainer
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
@PostgresTestContainer
@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,201 @@
package ltd.hlaeja.controller
import java.util.UUID
import ltd.hlaeja.library.deviceRegistry.Devices
import ltd.hlaeja.test.container.PostgresTestContainer
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody
@PostgresTestContainer
@SpringBootTest(webEnvironment = RANDOM_PORT)
class DevicesEndpoint {
@LocalServerPort
var port: Int = 0
lateinit var webClient: WebTestClient
@BeforeEach
fun setup() {
webClient = WebTestClient.bindToServer().baseUrl("http://localhost:$port").build()
}
@Nested
inner class GetDevices {
@Test
fun `get devices default - success`() {
// when
val result = webClient.get().uri("/devices").exchange()
// then
result.expectStatus().isOk()
.expectBody<List<Devices.Response>>()
.consumeWith {
assertThat(it.responseBody?.size).isEqualTo(4)
}
}
@ParameterizedTest
@CsvSource(
value = [
"1,4",
"2,0",
],
)
fun `get devices by page - success`(page: Int, expected: Int) {
// when
val result = webClient.get().uri("/devices/page-$page").exchange()
// then
result.expectStatus().isOk()
.expectBody<List<Devices.Response>>()
.consumeWith {
assertThat(it.responseBody?.size).isEqualTo(expected)
}
}
@Test
fun `get devices by pages - fail`() {
// when
val result = webClient.get().uri("/devices/page-0").exchange()
// then
result.expectStatus().isBadRequest
}
@ParameterizedTest
@CsvSource(
value = [
"1,2,2",
"2,2,2",
"3,2,0",
],
)
fun `get devices by page and show - success`(page: Int, show: Int, expected: Int) {
// when
val result = webClient.get().uri("/devices/page-$page/show-$show").exchange()
// then
result.expectStatus().isOk()
.expectBody<List<Devices.Response>>()
.consumeWith {
assertThat(it.responseBody?.size).isEqualTo(expected)
}
}
@ParameterizedTest
@CsvSource(
value = [
"0,1",
"1,0",
],
)
fun `get devices by page and show - fail`(page: Int, show: Int) {
// when
val result = webClient.get().uri("/devices/page-$page/show-$show").exchange()
// then
result.expectStatus().isBadRequest
}
}
@Nested
inner class GetDevicesByType {
@Test
fun `get devices for type default - success`() {
// when
val result = webClient.get()
.uri("/devices/type-00000000-0000-0000-0001-000000000001")
.exchange()
// then
result.expectStatus().isOk()
.expectBody<List<Devices.Response>>()
.consumeWith {
assertThat(it.responseBody?.size).isEqualTo(2)
}
}
@ParameterizedTest
@CsvSource(
value = [
"00000000-0000-0000-0001-000000000001,1,2",
"00000000-0000-0000-0001-000000000001,2,0",
],
)
fun `get devices for type by page - success`(type: UUID, page: Int, expected: Int) {
// when
val result = webClient.get()
.uri("/devices/type-$type/page-$page")
.exchange()
// then
result.expectStatus().isOk()
.expectBody<List<Devices.Response>>()
.consumeWith {
assertThat(it.responseBody?.size).isEqualTo(expected)
}
}
@Test
fun `get devices for type by pages - fail`() {
// when
val result = webClient.get()
.uri("/devices/type-00000000-0000-0000-0001-000000000001/page-0")
.exchange()
// then
result.expectStatus().isBadRequest
}
@ParameterizedTest
@CsvSource(
value = [
"00000000-0000-0000-0001-000000000001,1,1,1",
"00000000-0000-0000-0001-000000000001,2,1,1",
"00000000-0000-0000-0001-000000000001,3,1,0",
],
)
fun `get devices for type by page and show - success`(type: UUID, page: Int, show: Int, expected: Int) {
// when
val result = webClient.get()
.uri("/devices/type-$type/page-$page/show-$show")
.exchange()
// then
result.expectStatus().isOk()
.expectBody<List<Devices.Response>>()
.consumeWith {
assertThat(it.responseBody?.size).isEqualTo(expected)
}
}
@ParameterizedTest
@CsvSource(
value = [
"0,1",
"1,0",
],
)
fun `get devices for type by page and show - fail`(page: Int, show: Int) {
// when
val result = webClient.get()
.uri("/devices/type-00000000-0000-0000-0001-000000000001/page-$page/show-$show")
.exchange()
// then
result.expectStatus().isBadRequest
}
}
}

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.PostgresTestContainer
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
@PostgresTestContainer
@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.PostgresTestContainer
import org.assertj.core.api.SoftAssertions
import org.assertj.core.api.junit.jupiter.InjectSoftAssertions
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody
@PostgresTestContainer
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ExtendWith(SoftAssertionsExtension::class)
class 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,113 @@
package ltd.hlaeja.controller
import ltd.hlaeja.library.deviceRegistry.Nodes
import ltd.hlaeja.test.container.PostgresTestContainer
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.SoftAssertions
import org.assertj.core.api.junit.jupiter.InjectSoftAssertions
import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody
@PostgresTestContainer
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ExtendWith(SoftAssertionsExtension::class)
class NodesEndpoint {
@InjectSoftAssertions
lateinit var softly: SoftAssertions
@LocalServerPort
var port: Int = 0
lateinit var webClient: WebTestClient
@BeforeEach
fun setup() {
webClient = WebTestClient.bindToServer().baseUrl("http://localhost:$port").build()
}
@Test
fun `get nodes default - success`() {
// when
val result = webClient.get().uri("/nodes").exchange()
// then
result.expectStatus().isOk()
.expectBody<List<Nodes.Response>>()
.consumeWith {
assertThat(it.responseBody?.size).isEqualTo(3)
}
}
@ParameterizedTest
@CsvSource(
value = [
"1,3",
"2,0",
],
)
fun `get nodes by page - success`(page: Int, expected: Int) {
// when
val result = webClient.get().uri("/nodes/page-$page").exchange()
// then
result.expectStatus().isOk()
.expectBody<List<Nodes.Response>>()
.consumeWith {
assertThat(it.responseBody?.size).isEqualTo(expected)
}
}
@Test
fun `get nodes by pages - fail`() {
// when
val result = webClient.get().uri("/nodes/page-0").exchange()
// then
result.expectStatus().isBadRequest
}
@ParameterizedTest
@CsvSource(
value = [
"1,2,2",
"2,2,1",
"3,2,0",
],
)
fun `get nodes by page and show - success`(page: Int, show: Int, expected: Int) {
// when
val result = webClient.get().uri("/nodes/page-$page/show-$show").exchange()
// then
result.expectStatus().isOk()
.expectBody<List<Nodes.Response>>()
.consumeWith {
assertThat(it.responseBody?.size).isEqualTo(expected)
}
}
@ParameterizedTest
@CsvSource(
value = [
"0,1",
"1,0",
],
)
fun `get nodes by page and show - fail`(page: Int, show: Int) {
// when
val result = webClient.get().uri("/nodes/page-$page/show-$show").exchange()
// then
result.expectStatus().isBadRequest
}
}

View File

@@ -0,0 +1,291 @@
package ltd.hlaeja.controller
import ltd.hlaeja.library.deviceRegistry.Type
import ltd.hlaeja.test.container.PostgresTestContainer
import ltd.hlaeja.test.isEqualToUuid
import org.assertj.core.api.SoftAssertions
import org.assertj.core.api.junit.jupiter.InjectSoftAssertions
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
@PostgresTestContainer
@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,208 @@
package ltd.hlaeja.controller
import ltd.hlaeja.library.deviceRegistry.Types
import ltd.hlaeja.test.container.PostgresTestContainer
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody
@PostgresTestContainer
@SpringBootTest(webEnvironment = RANDOM_PORT)
class 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

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

@@ -0,0 +1,72 @@
package ltd.hlaeja.controller
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
import java.util.UUID
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.single
import kotlinx.coroutines.test.runTest
import ltd.hlaeja.entity.DeviceEntity
import ltd.hlaeja.service.DeviceService
import ltd.hlaeja.test.isEqualToUuid
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class DevicesControllerTest {
companion object {
const val NIL_UUID: String = "00000000-0000-0000-0000-000000000000"
val id: UUID = UUID.fromString(NIL_UUID)
val type: UUID = UUID.fromString(NIL_UUID)
val timestamp: ZonedDateTime = ZonedDateTime.of(LocalDateTime.of(2000, 1, 1, 0, 0, 0, 1), ZoneId.of("UTC"))
}
val service: DeviceService = mockk()
lateinit var controller: DevicesController
@BeforeEach
fun setUp() {
controller = DevicesController(service)
}
@Test
fun `get all devices`() = runTest {
// given
coEvery {
service.getDevices(any(), any())
} returns flowOf(DeviceEntity(id, timestamp, type))
// when
val response = controller.getDevices().single()
// then
coVerify(exactly = 1) { service.getDevices(0, 25) }
assertThat(response.id).isEqualToUuid(NIL_UUID)
assertThat(response.type).isEqualToUuid(NIL_UUID)
assertThat(response.timestamp).isEqualTo(timestamp)
}
@Test
fun `get all devices for type`() = runTest {
// given
coEvery {
service.getDevicesByType(any(), any(), any())
} returns flowOf(DeviceEntity(id, timestamp, type))
// when
val response = controller.getDevicesByType(type).single()
// then
coVerify(exactly = 1) { service.getDevicesByType(type, 0, 25) }
assertThat(response.id).isEqualToUuid(NIL_UUID)
assertThat(response.type).isEqualToUuid(NIL_UUID)
assertThat(response.timestamp).isEqualTo(timestamp)
}
}

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

@@ -0,0 +1,58 @@
package ltd.hlaeja.controller
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
import java.util.UUID
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.single
import kotlinx.coroutines.test.runTest
import ltd.hlaeja.entity.NodeEntity
import ltd.hlaeja.service.NodeService
import ltd.hlaeja.test.isEqualToUuid
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class NodesControllerTest {
companion object {
const val NAME: String = "My Device"
const val NIL_UUID: String = "00000000-0000-0000-0000-000000000000"
val id: UUID = UUID.fromString(NIL_UUID)
val client: UUID = UUID.fromString(NIL_UUID)
val device: UUID = UUID.fromString(NIL_UUID)
val timestamp: ZonedDateTime = ZonedDateTime.of(LocalDateTime.of(2000, 1, 1, 0, 0, 0, 1), ZoneId.of("UTC"))
}
val service: NodeService = mockk()
lateinit var controller: NodesController
@BeforeEach
fun setUp() {
controller = NodesController(service)
}
@Test
fun `get all nodes`() = runTest {
// given
coEvery {
service.getNodes(any(), any())
} returns flowOf(NodeEntity(id, timestamp, client, device, NAME))
// when
val response = controller.getNodes().single()
// then
coVerify(exactly = 1) { service.getNodes(0, 25) }
assertThat(response.id).isEqualToUuid(NIL_UUID)
assertThat(response.timestamp).isEqualTo(timestamp)
assertThat(response.client).isEqualToUuid(NIL_UUID)
assertThat(response.device).isEqualToUuid(NIL_UUID)
assertThat(response.name).isEqualTo(NAME)
}
}

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.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZonedDateTime
import java.util.UUID
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.single
import kotlinx.coroutines.test.runTest
import ltd.hlaeja.entity.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
coEvery {
service.getTypes(any(), any(), any())
} returns flowOf(TypeEntity(id, timestamp, NAME))
// when
val response = controller.getTypes().single()
// then
coVerify(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

@@ -4,76 +4,320 @@ 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.verify import io.mockk.mockkStatic
import java.time.Instant import io.mockk.unmockkStatic
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`() = runTest {
// given // given
every { repository.findAll() } returns flowOf(mockk<TypeEntity>()) coEvery { typeRepository.findAll(any(), any()) } returns flowOf(mockk<TypeEntity>())
// when // when
service.getTypes() service.getTypes(1, 10, null)
// then // then
verify(exactly = 1) { repository.findAll() } coVerify(exactly = 1) { typeRepository.findAll(1, 10) }
coVerify(exactly = 0) { typeRepository.findAllContaining(any(), any(), any()) }
}
@Test
fun `get all types with filter`() = runTest {
// given
coEvery { typeRepository.findAllContaining(any(), any(), any()) } returns flowOf(mockk<TypeEntity>())
// when
service.getTypes(1, 10, "abc")
// then
coVerify(exactly = 1) { typeRepository.findAllContaining("%abc%", 1, 10) }
coVerify(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")
}
}
} }