31 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
f52f1237a2 [RELEASE] - release version: 0.4.0 2025-01-02 07:24:12 +01:00
4130ba681c update addDevice to handle violates of foreign key in DeviceService 2025-01-02 06:53:20 +01:00
df9d2c59a4 replace local jwt with library version
- update DeviceController to handle hlaeja jwt instead of jwtService
- update mapper sign with hlaeja jwt instead of jwtService
- add dependency for hlaeja jwt
- remove dependencies for jjwt
- remove JwtService.kt
- remove PrivateKeyProvider.kt
- remove jwt key property explanation from additional-spring-configuration-metadata.json
2025-01-02 06:53:20 +01:00
7d4ebab8f8 [RELEASE] - bump version 2024-12-28 07:43:32 +01:00
7184854ed4 [RELEASE] - release version: 0.3.0 2024-12-28 07:43:29 +01:00
df8e1a7b48 update for getDevice
- add get device in DeviceController
- add get device in DeviceService
- update logging
- update and cleanup in README.md
2024-12-27 22:56:27 +01:00
e7dbbc7a78 [RELEASE] - bump version 2024-12-10 23:25:29 +01:00
2a69d06d7f [RELEASE] - release version: 0.2.0 2024-12-10 23:25:26 +01:00
d5746aa22d update for common messages v0.2.0
- change DeviceController addDevice to use toDeviceResponse
- add DeviceEntity.toDeviceResponse to Mapping.kt
2024-12-10 21:28:58 +01:00
5d36099738 update for common plugin v0.2.0
- rename keys folder to cert
- update build.gradle.kts to use certificate plugin
- update catalog version
2024-12-10 19:20:14 +01:00
5c2e2e617e [RELEASE] - bump version 2024-11-26 11:42:12 +01:00
80 changed files with 2613 additions and 536 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 }}

2
.gitignore vendored
View File

@@ -40,4 +40,4 @@ out/
.kotlin .kotlin
### Cert ### ### Cert ###
/keys/ /cert/

View File

@@ -5,49 +5,43 @@ Classes crafted, identities bestowed, Each device recorded, their functions unfo
## Properties for deployment ## Properties for deployment
| name | required | info | | name | required | info |
|------------------------|----------|-------------------------| |------------------------|:--------:|-------------------------|
| spring.profiles.active | * | Spring Boot environment | | spring.profiles.active | ✓ | Spring Boot environment |
| spring.r2dbc.url | * | Postgres host url | | spring.r2dbc.url | ✓ | Postgres host url |
| spring.r2dbc.username | * | Postgres username | | spring.r2dbc.username | ✓ | Postgres username |
| spring.r2dbc.password | ** | Postgres password | | spring.r2dbc.password | ✗ | Postgres password |
| jwt.private-key | | JWT private cert | | jwt.private-key | ✓ | JWT private cert |
Required: * can be stored as text, and ** need to be stored as secret. *Required: ✓ can be stored as text, and ✗ need to be stored as secret.*
## Releasing Service ## Releasing Service
Run `release.sh` script from `master` branch. Run release pipeline from `master` branch.
## Development Information ## Development Information
### Generate Private and Public RSA Key ### Private RSA Key
OpenSSL Project is dedicated to providing a simple installation of OpenSSL for Microsoft Windows. [Download](https://slproweb.com/products/Win32OpenSSL.html) This service uses RAS keys to create identities for devices. The private key is used here to generate identities, while the public key is used by **[Hlæja Device API](https://github.com/swordsteel/hlaeja-device-api)** to identify a device and accept data.
Generate an RSA private key, of size 2048, and output it to a file named `private_key.pem` in to `./keys` *For instructions on how to set these up, please refer to our [generate RSA key](https://github.com/swordsteel/hlaeja-development/blob/master/doc/rsa_key.md) documentation.*
```shell ### Global Setting
openssl genrsa -out private_key.pem 2048
The following global settings are used in Hlaeja Device Registry. You can configure these settings using either Gradle properties or alternatively environment variables.
*For instructions on how to set these up, please refer to our [set global settings](https://github.com/swordsteel/hlaeja-development/blob/master/doc/global_settings.md) documentation.*
#### Gradle Properties
```properties
repository.user=your_user
repository.token=your_token_value
``` ```
Extract the public key from `private_key.pem` from `./keys`, and output it to a file named `public_key.pem` in to `./keys` #### Environment Variables
```shell ```properties
openssl rsa -in private_key.pem -pubout -out public_key.pem REPOSITORY_USER=your_user
REPOSITORY_TOKEN=your_token_value
``` ```
### Global gradle properties
To authenticate with Gradle to access repositories that require authentication, you can set your user and token in the `gradle.properties` file.
Here's how you can do it:
1. Open or create the `gradle.properties` file in your Gradle user home directory:
- On Unix-like systems (Linux, macOS), this directory is typically `~/.gradle/`.
- On Windows, this directory is typically `C:\Users\<YourUsername>\.gradle\`.
2. Add the following lines to the `gradle.properties` file:
```properties
repository.user=your_user
repository.token=your_token_value
```
or use environment variables `REPOSITORY_USER` and `REPOSITORY_TOKEN`

View File

@@ -1,47 +1,48 @@
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.service) alias(hlaeja.plugins.spring.boot)
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 {
implementation(hlaeja.com.fasterxml.jackson.module.kotlin) implementation(hlaeja.fasterxml.jackson.module.kotlin)
implementation(hlaeja.jjwt.api)
implementation(hlaeja.kotlin.logging) implementation(hlaeja.kotlin.logging)
implementation(hlaeja.kotlin.reflect) implementation(hlaeja.kotlin.reflect)
implementation(hlaeja.kotlinx.coroutines) implementation(hlaeja.kotlinx.coroutines)
implementation(hlaeja.ltd.hlaeja.library.common.messages) implementation(hlaeja.library.common.messages)
implementation(hlaeja.org.springframework.springboot.actuator.starter) implementation(hlaeja.library.jwt)
implementation(hlaeja.org.springframework.springboot.r2dbc.starter) implementation(hlaeja.springboot.starter.actuator)
implementation(hlaeja.org.springframework.springboot.webflux.starter) implementation(hlaeja.springboot.starter.r2dbc)
implementation(hlaeja.springboot.starter.validation)
implementation(hlaeja.springboot.starter.webflux)
runtimeOnly(hlaeja.jjwt.impl) runtimeOnly(hlaeja.postgresql)
runtimeOnly(hlaeja.jjwt.jackson) runtimeOnly(hlaeja.postgresql.r2dbc)
runtimeOnly(hlaeja.org.postgresql)
runtimeOnly(hlaeja.org.postgresql.r2dbc)
testImplementation(hlaeja.assertj.core) testImplementation(hlaeja.assertj.core)
testImplementation(hlaeja.io.mockk) testImplementation(hlaeja.library.test)
testImplementation(hlaeja.io.projectreactor.reactor.test) testImplementation(hlaeja.mockk)
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.org.springframework.springboot.test.starter)
testRuntimeOnly(hlaeja.org.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"
tasks { tasks.named("processResources") {
named("processResources") { dependsOn("copyCertificates")
dependsOn("copyPrivateKey")
}
register<Copy>("copyPrivateKey") {
group = "hlaeja"
from("keys/private_key.pem")
into("${layout.buildDirectory.get()}/resources/main/keys")
onlyIf { file("keys/private_key.pem").exists() }
}
} }

View File

@@ -1,4 +1,4 @@
kotlin.code.style=official kotlin.code.style=official
version=0.1.0 version=0.7.0
catalog=0.4.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

View File

@@ -5,3 +5,6 @@ Content-Type: application/json
{ {
"type": "00000000-0000-0000-0000-000000000000" "type": "00000000-0000-0000-0000-000000000000"
} }
### register device for a type
GET {{hostname}}/device-00000000-0000-0000-0000-000000000000

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

@@ -5,13 +5,18 @@ 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

@@ -1,24 +1,31 @@
package ltd.hlaeja.controller package ltd.hlaeja.controller
import java.util.UUID
import ltd.hlaeja.jwt.service.PrivateJwtService
import ltd.hlaeja.library.deviceRegistry.Device import ltd.hlaeja.library.deviceRegistry.Device
import ltd.hlaeja.service.DeviceService import ltd.hlaeja.service.DeviceService
import ltd.hlaeja.service.JwtService import ltd.hlaeja.util.toDeviceResponse
import org.springframework.http.HttpStatus.EXPECTATION_FAILED 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.RequestBody import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import org.springframework.web.server.ResponseStatusException
@RestController @RestController
class DeviceController( class DeviceController(
private val deviceService: DeviceService, private val deviceService: DeviceService,
private val jwtService: JwtService, private val privateJwtService: PrivateJwtService,
) { ) {
@PostMapping("/device") @PostMapping("/device")
suspend fun addDevice( suspend fun addDevice(
@RequestBody request: Device.Request, @RequestBody request: Device.Request,
): Device.Identity = deviceService.addDevice(request.type) ): Device.Response = deviceService.addDevice(request.type)
.let { jwtService.makeIdentity(it.id ?: throw ResponseStatusException(EXPECTATION_FAILED)) } .toDeviceResponse(privateJwtService)
.let { Device.Identity(it) }
@GetMapping("/device-{device}")
suspend fun getDevice(
@PathVariable device: UUID,
): Device.Response = deviceService.getDevice(device)
.toDeviceResponse(privateJwtService)
} }

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

@@ -1,11 +1,16 @@
package ltd.hlaeja.service package ltd.hlaeja.service
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 mu.KotlinLogging import org.springframework.dao.DataIntegrityViolationException
import org.springframework.http.HttpStatus.BAD_REQUEST
import org.springframework.http.HttpStatus.NOT_FOUND
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.web.server.ResponseStatusException
private val log = KotlinLogging.logger {} private val log = KotlinLogging.logger {}
@@ -16,6 +21,26 @@ class DeviceService(
suspend fun addDevice( suspend fun addDevice(
type: UUID, type: UUID,
): DeviceEntity = deviceRepository.save(DeviceEntity(null, ZonedDateTime.now(), type)) ): DeviceEntity = try {
deviceRepository.save(DeviceEntity(null, ZonedDateTime.now(), type))
.also { log.debug { "Added device ${it.id}" } } .also { log.debug { "Added device ${it.id}" } }
} catch (e: DataIntegrityViolationException) {
log.warn { e.localizedMessage }
throw ResponseStatusException(BAD_REQUEST)
}
suspend fun getDevice(device: UUID): DeviceEntity = deviceRepository.findById(device)
?.also { log.debug { "Get device ${it.id}" } }
?: 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

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

View File

@@ -1,9 +1,12 @@
package ltd.hlaeja.service package ltd.hlaeja.service
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 mu.KotlinLogging 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 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 mu.KotlinLogging import org.springframework.dao.DataIntegrityViolationException
import org.springframework.dao.DuplicateKeyException import org.springframework.http.HttpStatus.ACCEPTED
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus.CONFLICT
import org.springframework.http.HttpStatus.EXPECTATION_FAILED
import org.springframework.http.HttpStatus.NOT_FOUND
import org.springframework.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,19 +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.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.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(
@@ -36,3 +62,25 @@ fun NodeEntity.toIdentityResponse(): Identity.Response = Identity.Response(
id ?: throw ResponseStatusException(EXPECTATION_FAILED), id ?: throw ResponseStatusException(EXPECTATION_FAILED),
device, device,
) )
fun DeviceEntity.toDeviceResponse(
jwtService: PrivateJwtService,
): Device.Response = Device.Response(
id ?: throw ResponseStatusException(EXPECTATION_FAILED),
type,
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

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

View File

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

View File

@@ -10,8 +10,22 @@ 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: keys/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

@@ -1,5 +1,5 @@
jwt: jwt:
private-key: keys/valid-private-key.pem private-key: cert/valid-private-key.pem
spring: spring:
r2dbc: r2dbc:

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

@@ -9,11 +9,12 @@ import java.time.ZonedDateTime
import java.util.UUID import java.util.UUID
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import ltd.hlaeja.entity.DeviceEntity import ltd.hlaeja.entity.DeviceEntity
import ltd.hlaeja.jwt.service.PrivateJwtService
import ltd.hlaeja.library.deviceRegistry.Device import ltd.hlaeja.library.deviceRegistry.Device
import ltd.hlaeja.service.DeviceService import ltd.hlaeja.service.DeviceService
import ltd.hlaeja.service.JwtService
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
import org.springframework.web.server.ResponseStatusException import org.springframework.web.server.ResponseStatusException
@@ -27,28 +28,31 @@ class DeviceControllerTest {
} }
val deviceService: DeviceService = mockk() val deviceService: DeviceService = mockk()
val jwtService: JwtService = mockk() val privateJwtService: PrivateJwtService = mockk()
lateinit var controller: DeviceController lateinit var controller: DeviceController
@BeforeEach @BeforeEach
fun setUp() { fun setUp() {
controller = DeviceController(deviceService, jwtService) controller = DeviceController(deviceService, privateJwtService)
} }
@Nested
inner class AddDeviceTest {
@Test @Test
fun `add device - success`() = runTest { fun `add device - success`() = runTest {
// given // given
val request = Device.Request(uuid) val request = Device.Request(uuid)
coEvery { deviceService.addDevice(any()) } returns DeviceEntity(uuid, timestamp, uuid) coEvery { deviceService.addDevice(any()) } returns DeviceEntity(uuid, timestamp, uuid)
coEvery { jwtService.makeIdentity(any()) } returns PAYLOAD coEvery { privateJwtService.sign(any()) } returns PAYLOAD
// when // when
val response = controller.addDevice(request) val response = controller.addDevice(request)
// then // then
coVerify(exactly = 1) { deviceService.addDevice(any()) } coVerify(exactly = 1) { deviceService.addDevice(any()) }
coVerify(exactly = 1) { jwtService.makeIdentity(any()) } coVerify(exactly = 1) { privateJwtService.sign(any()) }
assertThat(response.identity).isEqualTo(PAYLOAD) assertThat(response.identity).isEqualTo(PAYLOAD)
} }
@@ -67,4 +71,25 @@ class DeviceControllerTest {
// then // then
assertThat(exception.message).isEqualTo("417 EXPECTATION_FAILED") assertThat(exception.message).isEqualTo("417 EXPECTATION_FAILED")
} }
}
@Nested
inner class GetDeviceTest {
@Test
fun `get device - success`() = runTest {
// given
coEvery { deviceService.getDevice(any()) } returns DeviceEntity(uuid, timestamp, uuid)
coEvery { privateJwtService.sign(any()) } returns PAYLOAD
// when
val response = controller.getDevice(uuid)
// then
coVerify(exactly = 1) { deviceService.getDevice(any()) }
coVerify(exactly = 1) { privateJwtService.sign(any()) }
assertThat(response.identity).isEqualTo(PAYLOAD)
}
}
} }

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

@@ -17,6 +17,8 @@ import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterEach 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.junit.jupiter.api.assertThrows
import org.springframework.web.server.ResponseStatusException
class DeviceServiceTest { class DeviceServiceTest {
companion object { companion object {
@@ -42,7 +44,7 @@ class DeviceServiceTest {
} }
@Test @Test
fun `add new type success`() = runTest { fun `add new device success`() = runTest {
// given // given
coEvery { repository.save(any()) } answers { call -> coEvery { repository.save(any()) } answers { call ->
(call.invocation.args[0] as DeviceEntity).copy(id = device) (call.invocation.args[0] as DeviceEntity).copy(id = device)
@@ -58,4 +60,37 @@ class DeviceServiceTest {
assertThat(result.timestamp.toString()).isEqualTo("2000-01-01T00:00:00.001Z[UTC]") assertThat(result.timestamp.toString()).isEqualTo("2000-01-01T00:00:00.001Z[UTC]")
assertThat(result.type).isEqualTo(type) assertThat(result.type).isEqualTo(type)
} }
@Test
fun `get device - success`() = runTest {
// given
val device = UUID.fromString("00000000-0000-0000-0000-000000000000")
val entity: DeviceEntity = mockk()
coEvery { repository.findById(any()) } returns entity
coEvery { entity.id } returns device
// when
val result = service.getDevice(type)
// then
coVerify(exactly = 1) { repository.findById(any()) }
assertThat(result.id).isEqualTo(device)
}
@Test
fun `get device - fail not found`() = runTest {
// given
val device = UUID.fromString("00000000-0000-0000-0000-000000000000")
coEvery { repository.findById(any()) } returns null
// when
val exception = assertThrows<ResponseStatusException> {
service.getDevice(device)
}
// then
assertThat(exception.message).isEqualTo("404 NOT_FOUND")
}
} }

View File

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

View File

@@ -4,76 +4,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,
description = call.invocation.args[1] as String,
) )
}
coEvery { repository.save(any()) } answers { call -> (call.invocation.args[0] as TypeEntity).copy(id = uuid) }
// when // when
service.addType(entity) val result = service.addType("name", "description")
// then // then
coVerify(exactly = 1) { repository.save(any()) } coVerify(exactly = 1) { typeRepository.save(any()) }
coVerify(exactly = 1) { typeDescriptionRepository.upsert(any(), any()) }
assertThat(result.id).isEqualToUuid("00000000-0000-0000-0000-000000000000")
assertThat(result.timestamp).isEqualTo(timestamp)
assertThat(result.name).isEqualTo("name")
assertThat(result.description).isEqualTo("description")
} }
@Test @Test
fun `add new type exception`() = runTest { fun `add new type - fail this should never happen save not updating id`() = runTest {
// given // given
val entity: TypeEntity = mockk() coEvery { typeRepository.save(any()) } answers { call -> call.invocation.args[0] as TypeEntity }
coEvery { repository.save(any()) } throws DuplicateKeyException("duplicate key") // when exception
val response: ResponseStatusException = assertFailsWith<ResponseStatusException> {
// then exception service.addType("name", "description")
assertFailsWith<ResponseStatusException> {
service.addType(entity)
} }
// then
coVerify(exactly = 1) { typeRepository.save(any()) }
coVerify(exactly = 0) { typeDescriptionRepository.upsert(any(), any()) }
assertThat(response.statusCode).isEqualTo(EXPECTATION_FAILED)
}
@Test
fun `add new type - fail duplicate key`() = runTest {
// given
coEvery { typeRepository.save(any()) } throws DuplicateKeyException("duplicate key")
// when exception
val response: ResponseStatusException = assertFailsWith<ResponseStatusException> {
service.addType("name", "description")
}
// then
coVerify(exactly = 1) { typeRepository.save(any()) }
coVerify(exactly = 0) { typeDescriptionRepository.upsert(any(), any()) }
assertThat(response.statusCode).isEqualTo(CONFLICT)
}
@Test
fun `get type - success`() = runTest {
// given
coEvery { typeRepository.findTypeWithDescription(any()) } answers { call ->
TypeWithDescription(
id = call.invocation.args[0] as UUID,
timestamp = timestamp,
name = "name",
description = "description",
)
}
// when
val result = service.getType(uuid)
// then
coVerify(exactly = 1) { typeRepository.findTypeWithDescription(any()) }
assertThat(result.id).isEqualToUuid("00000000-0000-0000-0000-000000000000")
assertThat(result.timestamp).isEqualTo(timestamp)
assertThat(result.name).isEqualTo("name")
assertThat(result.description).isEqualTo("description")
}
@Test
fun `get type - fail`() = runTest {
// given
coEvery { typeRepository.findTypeWithDescription(any()) } returns null
// when exception
val response: ResponseStatusException = assertFailsWith<ResponseStatusException> {
service.getType(uuid)
}
// then
coVerify(exactly = 1) { typeRepository.findTypeWithDescription(any()) }
assertThat(response.statusCode).isEqualTo(NOT_FOUND)
}
@Test
fun `update type - success`() = runTest {
// given
coEvery { typeRepository.findById(any()) } answers { call ->
TypeEntity(
id = call.invocation.args[0] as UUID,
timestamp = timestamp,
name = "name",
)
}
coEvery { typeRepository.save(any()) } answers { call ->
call.invocation.args[0] as TypeEntity
}
coEvery { typeDescriptionRepository.findById(any()) } answers { call ->
TypeDescriptionEntity(
typeId = call.invocation.args[0] as UUID,
description = "description",
)
}
coEvery { typeDescriptionRepository.save(any()) } answers { call ->
call.invocation.args[0] as TypeDescriptionEntity
}
// when
val result = service.updateType(uuid, "new-name", "new-description")
// then
coVerify(exactly = 1) { typeRepository.findById(any()) }
coVerify(exactly = 1) { typeRepository.save(any()) }
coVerify(exactly = 1) { typeDescriptionRepository.findById(any()) }
coVerify(exactly = 1) { typeDescriptionRepository.save(any()) }
assertThat(result.id).isEqualToUuid("00000000-0000-0000-0000-000000000000")
assertThat(result.timestamp).isEqualTo(timestamp)
assertThat(result.name).isEqualTo("new-name")
assertThat(result.description).isEqualTo("new-description")
}
@Test
fun `update type - success no change`() = runTest {
// given
coEvery { typeRepository.findById(any()) } answers { call ->
TypeEntity(
id = call.invocation.args[0] as UUID,
timestamp = timestamp,
name = "name",
)
}
coEvery { typeDescriptionRepository.findById(any()) } answers { call ->
TypeDescriptionEntity(
typeId = call.invocation.args[0] as UUID,
description = "description",
)
}
// when exception
val response: ResponseStatusException = assertFailsWith<ResponseStatusException> {
service.updateType(uuid, "name", "description")
}
// then
coVerify(exactly = 1) { typeRepository.findById(any()) }
coVerify(exactly = 0) { typeRepository.save(any()) }
coVerify(exactly = 1) { typeDescriptionRepository.findById(any()) }
coVerify(exactly = 0) { typeDescriptionRepository.save(any()) }
assertThat(response.statusCode).isEqualTo(ACCEPTED)
}
@Test
fun `update type - fail type dont exist`() = runTest {
// given
coEvery { typeRepository.findById(any()) } returns null
// when exception
val response: ResponseStatusException = assertFailsWith<ResponseStatusException> {
service.updateType(uuid, "name", "description")
}
// then
coVerify(exactly = 1) { typeRepository.findById(any()) }
coVerify(exactly = 0) { typeRepository.save(any()) }
coVerify(exactly = 0) { typeDescriptionRepository.findById(any()) }
coVerify(exactly = 0) { typeDescriptionRepository.save(any()) }
assertThat(response.statusCode).isEqualTo(NOT_FOUND)
}
@Test
fun `update type - fail type description dont exist`() = runTest {
// given
coEvery { typeRepository.findById(any()) } answers { call ->
TypeEntity(
id = call.invocation.args[0] as UUID,
timestamp = timestamp,
name = "name",
)
}
coEvery { typeDescriptionRepository.findById(any()) } returns null
// when exception
val response: ResponseStatusException = assertFailsWith<ResponseStatusException> {
service.updateType(uuid, "name", "description")
}
// then
coVerify(exactly = 1) { typeRepository.findById(any()) }
coVerify(exactly = 0) { typeRepository.save(any()) }
coVerify(exactly = 1) { typeDescriptionRepository.findById(any()) }
coVerify(exactly = 0) { typeDescriptionRepository.save(any()) }
assertThat(response.statusCode).isEqualTo(NOT_FOUND)
}
@Test
fun `update type - fail name already exists`() = runTest {
// given
coEvery { typeRepository.findById(any()) } answers { call ->
TypeEntity(
id = call.invocation.args[0] as UUID,
timestamp = timestamp,
name = "name",
)
}
coEvery { typeRepository.save(any()) } throws DuplicateKeyException("duplicate key")
// when exception
val response: ResponseStatusException = assertFailsWith<ResponseStatusException> {
service.updateType(uuid, "taken-name", "description")
}
// then
coVerify(exactly = 1) { typeRepository.findById(any()) }
coVerify(exactly = 1) { typeRepository.save(any()) }
coVerify(exactly = 0) { typeDescriptionRepository.findById(any()) }
coVerify(exactly = 0) { typeDescriptionRepository.save(any()) }
assertThat(response.statusCode).isEqualTo(CONFLICT)
} }
} }

View File

@@ -1,6 +1,7 @@
package ltd.hlaeja.util package ltd.hlaeja.util
import io.mockk.every import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic import io.mockk.mockkStatic
import io.mockk.unmockkStatic import io.mockk.unmockkStatic
import java.time.LocalDateTime import java.time.LocalDateTime
@@ -8,11 +9,14 @@ import java.time.ZoneId
import java.time.ZonedDateTime import java.time.ZonedDateTime
import java.util.UUID import java.util.UUID
import kotlin.test.Test import kotlin.test.Test
import ltd.hlaeja.assertj.assertThat import ltd.hlaeja.dto.TypeWithDescription
import ltd.hlaeja.entity.DeviceEntity
import ltd.hlaeja.entity.NodeEntity import ltd.hlaeja.entity.NodeEntity
import ltd.hlaeja.entity.TypeEntity import ltd.hlaeja.entity.TypeEntity
import ltd.hlaeja.library.deviceRegistry.Type import ltd.hlaeja.jwt.service.PrivateJwtService
import ltd.hlaeja.library.deviceRegistry.Node import ltd.hlaeja.library.deviceRegistry.Node
import ltd.hlaeja.library.deviceRegistry.Type
import ltd.hlaeja.test.isEqualToUuid
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertThrows import org.junit.jupiter.api.Assertions.assertThrows
@@ -40,23 +44,82 @@ class MappingKtTest {
inner class TypeMapping { inner class TypeMapping {
@Test @Test
fun `request to entity successful`() { fun `request to type entity successful`() {
// given // given
val id = UUID.fromString("00000000-0000-0000-0000-000000000001")
val request = Type.Request( val request = Type.Request(
"test", "name",
"description",
) )
// when // when
val result = request.toTypeEntity() val entity = request.toTypeEntity(id)
// then // then
assertThat(result.id).isNull() assertThat(entity.id).isEqualToUuid("00000000-0000-0000-0000-000000000001")
assertThat(result.timestamp.toString()).isEqualTo("2000-01-01T00:00:00.000000001Z[UTC]") assertThat(entity.timestamp).isEqualTo(timestamp)
assertThat(result.name).isEqualTo("test") assertThat(entity.name).isEqualTo("name")
} }
@Test @Test
fun `entity to response successful`() { fun `request to type description entity successful`() {
// given
val id = UUID.fromString("00000000-0000-0000-0000-000000000001")
val request = Type.Request(
"name",
"description",
)
// when
val entity = request.toTypeDescriptionEntity(id)
// then
assertThat(entity.typeId).isEqualToUuid("00000000-0000-0000-0000-000000000001")
assertThat(entity.description).isEqualTo("description")
}
@Test
fun `type with description to response successful`() {
// given
val typeWithDescription = TypeWithDescription(
UUID.fromString("00000000-0000-0000-0000-000000000001"),
timestamp,
"name",
"description",
)
// when
val response = typeWithDescription.toTypeResponse()
// then
assertThat(response.id).isEqualToUuid("00000000-0000-0000-0000-000000000001")
assertThat(response.timestamp).isEqualTo(timestamp)
assertThat(response.name).isEqualTo("name")
assertThat(response.description).isEqualTo("description")
}
@Test
fun `type with description to response, description null successful`() {
// given
val typeWithDescription = TypeWithDescription(
UUID.fromString("00000000-0000-0000-0000-000000000001"),
timestamp,
"name",
null,
)
// when
val response = typeWithDescription.toTypeResponse()
// then
assertThat(response.id).isEqualToUuid("00000000-0000-0000-0000-000000000001")
assertThat(response.timestamp).isEqualTo(timestamp)
assertThat(response.name).isEqualTo("name")
assertThat(response.description).isEmpty()
}
@Test
fun `type entity to response successful`() {
// given // given
val entity = TypeEntity( val entity = TypeEntity(
UUID.fromString("00000000-0000-0000-0000-000000000000"), UUID.fromString("00000000-0000-0000-0000-000000000000"),
@@ -65,20 +128,13 @@ class MappingKtTest {
) )
// when // when
val response = entity.toTypeResponse() val response = entity.toTypesResponse()
// then // then
assertThat(response.id).isUUID("00000000-0000-0000-0000-000000000000") assertThat(response.id).isEqualToUuid("00000000-0000-0000-0000-000000000000")
assertThat(response.timestamp).isEqualTo(timestamp)
assertThat(response.name).isEqualTo("name") assertThat(response.name).isEqualTo("name")
} }
@Test
fun `entity to response exception`() {
// then exception
assertThrows(ResponseStatusException::class.java) {
TypeEntity(null, timestamp, "name").toTypeResponse()
}
}
} }
@Nested @Nested
@@ -98,7 +154,7 @@ class MappingKtTest {
// then // then
assertThat(result.id).isNull() assertThat(result.id).isNull()
assertThat(result.timestamp.toString()).isEqualTo("2000-01-01T00:00:00.000000001Z[UTC]") assertThat(result.timestamp).isEqualTo(timestamp)
assertThat(result.client.toString()).isEqualTo("00000000-0000-0000-0000-000000000001") assertThat(result.client.toString()).isEqualTo("00000000-0000-0000-0000-000000000001")
assertThat(result.device.toString()).isEqualTo("00000000-0000-0000-0000-000000000002") assertThat(result.device.toString()).isEqualTo("00000000-0000-0000-0000-000000000002")
assertThat(result.name).isEqualTo("test") assertThat(result.name).isEqualTo("test")
@@ -119,9 +175,9 @@ class MappingKtTest {
val result = entity.toNodeResponse() val result = entity.toNodeResponse()
// then // then
assertThat(result.id).isUUID("00000000-0000-0000-0000-000000000001") assertThat(result.id).isEqualToUuid("00000000-0000-0000-0000-000000000001")
assertThat(result.client).isUUID("00000000-0000-0000-0000-000000000002") assertThat(result.client).isEqualToUuid("00000000-0000-0000-0000-000000000002")
assertThat(result.device).isUUID("00000000-0000-0000-0000-000000000003") assertThat(result.device).isEqualToUuid("00000000-0000-0000-0000-000000000003")
assertThat(result.name).isEqualTo("test") assertThat(result.name).isEqualTo("test")
} }
@@ -164,9 +220,9 @@ class MappingKtTest {
val result = entity.toIdentityResponse() val result = entity.toIdentityResponse()
// then // then
assertThat(result.node).isUUID("00000000-0000-0000-0000-000000000001") assertThat(result.node).isEqualToUuid("00000000-0000-0000-0000-000000000001")
assertThat(result.client).isUUID("00000000-0000-0000-0000-000000000002") assertThat(result.client).isEqualToUuid("00000000-0000-0000-0000-000000000002")
assertThat(result.device).isUUID("00000000-0000-0000-0000-000000000003") assertThat(result.device).isEqualToUuid("00000000-0000-0000-0000-000000000003")
} }
@Test @Test
@@ -189,4 +245,48 @@ class MappingKtTest {
assertThat(exception.message).isEqualTo("417 EXPECTATION_FAILED") assertThat(exception.message).isEqualTo("417 EXPECTATION_FAILED")
} }
} }
@Nested
inner class DeviceMapping {
val jwtService: PrivateJwtService = mockk()
@Test
fun `entity to identity response successful`() {
// given
val entity = DeviceEntity(
UUID.fromString("00000000-0000-0000-0000-000000000001"),
timestamp,
UUID.fromString("00000000-0000-0000-0000-000000000002"),
)
every { jwtService.sign(any()) } returns "header.payload.signature"
// when
val result = entity.toDeviceResponse(jwtService)
// then
assertThat(result.id).isEqualToUuid("00000000-0000-0000-0000-000000000001")
assertThat(result.type).isEqualToUuid("00000000-0000-0000-0000-000000000002")
assertThat(result.identity).isEqualTo("header.payload.signature")
}
@Test
fun `entity to identity response exception`() {
// given
val entity = DeviceEntity(
null,
timestamp,
UUID.fromString("00000000-0000-0000-0000-000000000002"),
)
// then exception
val exception = assertThrows(ResponseStatusException::class.java) {
entity.toDeviceResponse(jwtService)
}
// then
assertThat(exception.message).isEqualTo("417 EXPECTATION_FAILED")
}
}
} }

View File

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

View File

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