diff --git a/.editorconfig b/.editorconfig index fc8945f..ac53f09 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,6 +14,10 @@ max_line_length = 1024 indent_size = 2 tab_width = 2 +[*.data] +max_line_length = 1024 +insert_final_newline = false + [*.bat] end_of_line = crlf diff --git a/build.gradle.kts b/build.gradle.kts index 46c3c54..0624103 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,8 +12,8 @@ dependencies { implementation(hlaeja.kotlin.logging) implementation(hlaeja.kotlin.reflect) implementation(hlaeja.kotlinx.coroutines) - implementation(hlaeja.library.hlaeja.common.messages) - implementation(hlaeja.library.hlaeja.jwt) + implementation(hlaeja.library.common.messages) + implementation(hlaeja.library.jwt) implementation(hlaeja.springboot.starter.actuator) implementation(hlaeja.springboot.starter.r2dbc) implementation(hlaeja.springboot.starter.webflux) @@ -26,9 +26,17 @@ dependencies { testImplementation(hlaeja.projectreactor.reactor.test) testImplementation(hlaeja.kotlin.test.junit5) testImplementation(hlaeja.kotlinx.coroutines.test) - testImplementation(hlaeja.springboot.starter.test) testRuntimeOnly(hlaeja.junit.platform.launcher) + + integrationTestImplementation(hlaeja.assertj.core) + integrationTestImplementation(hlaeja.library.test) + integrationTestImplementation(hlaeja.projectreactor.reactor.test) + integrationTestImplementation(hlaeja.kotlin.test.junit5) + integrationTestImplementation(hlaeja.kotlinx.coroutines.test) + integrationTestImplementation(hlaeja.springboot.starter.test) + + integrationTestRuntimeOnly(hlaeja.junit.platform.launcher) } group = "ltd.hlaeja" diff --git a/gradle.properties b/gradle.properties index d8a88dc..5884925 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ kotlin.code.style=official version=0.5.0-SNAPSHOT -catalog=0.8.0 +catalog=0.10.0-SNAPSHOT container.port.host=9010 diff --git a/src/integration-test/kotlin/ltd/hlaeja/ApplicationTests.kt b/src/integration-test/kotlin/ltd/hlaeja/ApplicationTests.kt new file mode 100644 index 0000000..30487e8 --- /dev/null +++ b/src/integration-test/kotlin/ltd/hlaeja/ApplicationTests.kt @@ -0,0 +1,10 @@ +package ltd.hlaeja + +@org.springframework.boot.test.context.SpringBootTest +class ApplicationTests { + + @org.junit.jupiter.api.Test + fun contextLoads() { + // place holder + } +} diff --git a/src/integration-test/kotlin/ltd/hlaeja/controller/DeviceEndpoint.kt b/src/integration-test/kotlin/ltd/hlaeja/controller/DeviceEndpoint.kt new file mode 100644 index 0000000..eccf1d3 --- /dev/null +++ b/src/integration-test/kotlin/ltd/hlaeja/controller/DeviceEndpoint.kt @@ -0,0 +1,96 @@ +package ltd.hlaeja.controller + +import java.util.UUID +import ltd.hlaeja.library.deviceRegistry.Device +import ltd.hlaeja.test.compareToFile +import ltd.hlaeja.test.container.PostgresContainer +import ltd.hlaeja.test.isEqualToUuid +import org.assertj.core.api.SoftAssertions +import org.assertj.core.api.junit.jupiter.InjectSoftAssertions +import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT +import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.reactive.server.expectBody + +@PostgresContainer +@SpringBootTest(webEnvironment = RANDOM_PORT) +@ExtendWith(SoftAssertionsExtension::class) +class DeviceEndpoint { + + @InjectSoftAssertions + lateinit var softly: SoftAssertions + + @LocalServerPort + var port: Int = 0 + + lateinit var webClient: WebTestClient + + @BeforeEach + fun setup() { + webClient = WebTestClient.bindToServer().baseUrl("http://localhost:$port").build() + } + + @Nested + inner class GetDevice { + + @Test + fun `get account - success valid uuid`() { + // given + val uuid = UUID.fromString("00000000-0000-0000-0002-000000000001") + + // when + val result = webClient.get().uri("/device-$uuid").exchange() + + // then + result.expectStatus().isOk() + .expectBody() + .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() + .consumeWith { + softly.assertThat(it.responseBody?.id?.version()).isEqualTo(7) + softly.assertThat(it.responseBody?.type).isEqualTo(uuid) + } + } + } +} diff --git a/src/integration-test/kotlin/ltd/hlaeja/controller/IdentityEndpoint.kt b/src/integration-test/kotlin/ltd/hlaeja/controller/IdentityEndpoint.kt new file mode 100644 index 0000000..98f121a --- /dev/null +++ b/src/integration-test/kotlin/ltd/hlaeja/controller/IdentityEndpoint.kt @@ -0,0 +1,79 @@ +package ltd.hlaeja.controller + +import java.util.UUID +import ltd.hlaeja.library.deviceRegistry.Identity +import ltd.hlaeja.test.container.PostgresContainer +import ltd.hlaeja.test.isEqualToUuid +import org.assertj.core.api.SoftAssertions +import org.assertj.core.api.junit.jupiter.InjectSoftAssertions +import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT +import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.reactive.server.expectBody + +@PostgresContainer +@SpringBootTest(webEnvironment = RANDOM_PORT) +@ExtendWith(SoftAssertionsExtension::class) +class IdentityEndpoint { + + @InjectSoftAssertions + lateinit var softly: SoftAssertions + + @LocalServerPort + var port: Int = 0 + + lateinit var webClient: WebTestClient + + @BeforeEach + fun setup() { + webClient = WebTestClient.bindToServer().baseUrl("http://localhost:$port").build() + } + + @Test + fun `get identity - success`() { + // given + val device = UUID.fromString("00000000-0000-0000-0002-000000000002") + + // when + val result = webClient.get().uri("/identity/device-$device").exchange() + + // then + result.expectStatus() + .isOk + .expectBody() + .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 + } +} diff --git a/src/integration-test/kotlin/ltd/hlaeja/controller/NodeEndpoint.kt b/src/integration-test/kotlin/ltd/hlaeja/controller/NodeEndpoint.kt new file mode 100644 index 0000000..79da16b --- /dev/null +++ b/src/integration-test/kotlin/ltd/hlaeja/controller/NodeEndpoint.kt @@ -0,0 +1,58 @@ +package ltd.hlaeja.controller + +import java.util.UUID +import ltd.hlaeja.library.deviceRegistry.Node +import ltd.hlaeja.test.container.PostgresContainer +import org.assertj.core.api.SoftAssertions +import org.assertj.core.api.junit.jupiter.InjectSoftAssertions +import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT +import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.reactive.server.expectBody + +@PostgresContainer +@SpringBootTest(webEnvironment = RANDOM_PORT) +@ExtendWith(SoftAssertionsExtension::class) +class NodeEndpoint { + + @InjectSoftAssertions + lateinit var softly: SoftAssertions + + @LocalServerPort + var port: Int = 0 + + lateinit var webClient: WebTestClient + + @BeforeEach + fun setup() { + webClient = WebTestClient.bindToServer().baseUrl("http://localhost:$port").build() + } + + @Test + fun `added node - success`() { + // given + val name = "Node 4" + val device = UUID.fromString("00000000-0000-0000-0002-000000000001") + val client = UUID.fromString("00000000-0000-0000-0000-000000000000") + val request = Node.Request(device = device, client = client, name = name) + + // when + val result = webClient.post().uri("/node").bodyValue(request).exchange() + + // then + result.expectStatus() + .isOk + .expectBody() + .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) + } + } +} diff --git a/src/integration-test/kotlin/ltd/hlaeja/controller/TypeEndpoint.kt b/src/integration-test/kotlin/ltd/hlaeja/controller/TypeEndpoint.kt new file mode 100644 index 0000000..1fc0be7 --- /dev/null +++ b/src/integration-test/kotlin/ltd/hlaeja/controller/TypeEndpoint.kt @@ -0,0 +1,93 @@ +package ltd.hlaeja.controller + +import ltd.hlaeja.library.deviceRegistry.Type +import ltd.hlaeja.test.container.PostgresContainer +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.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.http.HttpStatus.CONFLICT +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.reactive.server.expectBody + +@PostgresContainer +@SpringBootTest(webEnvironment = RANDOM_PORT) +@ExtendWith(SoftAssertionsExtension::class) +class TypeEndpoint { + + @InjectSoftAssertions + lateinit var softly: SoftAssertions + + @LocalServerPort + var port: Int = 0 + + lateinit var webClient: WebTestClient + + @BeforeEach + fun setup() { + webClient = WebTestClient.bindToServer().baseUrl("http://localhost:$port").build() + } + + @Nested + inner class GetTypes { + + @Test + fun `get types`() { + // when + val result = webClient.get().uri("/types").exchange() + + // then + result.expectStatus().isOk() + .expectBody>() + .consumeWith { + assertThat(it.responseBody?.size).isEqualTo(5) + } + } + } + + @Nested + inner class CreateType { + + @Test + fun `added type - success`() { + // given + val name = "Thing 5" + val request = Type.Request( + name = name, + ) + + // when + val result = webClient.post().uri("/type").bodyValue(request).exchange() + + // then + result.expectStatus() + .isOk + .expectBody() + .consumeWith { + softly.assertThat(it.responseBody?.id?.version()).isEqualTo(7) + softly.assertThat(it.responseBody?.name).isEqualTo(name) + } + } + + @Test + fun `added type - fail name take`() { + // given + val request = Type.Request( + name = "Thing 1", + ) + + // when + val result = webClient.post().uri("/type").bodyValue(request).exchange() + + // then + result.expectStatus().isEqualTo(CONFLICT) + } + } +} diff --git a/src/test/resources/application.yml b/src/integration-test/resources/application.yml similarity index 100% rename from src/test/resources/application.yml rename to src/integration-test/resources/application.yml diff --git a/src/test/resources/cert/valid-private-key.pem b/src/integration-test/resources/cert/valid-private-key.pem similarity index 100% rename from src/test/resources/cert/valid-private-key.pem rename to src/integration-test/resources/cert/valid-private-key.pem diff --git a/src/integration-test/resources/identity/first-device.data b/src/integration-test/resources/identity/first-device.data new file mode 100644 index 0000000..efb4cd1 --- /dev/null +++ b/src/integration-test/resources/identity/first-device.data @@ -0,0 +1 @@ +eyJhbGciOiJSUzI1NiJ9.eyJkZXZpY2UiOiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMi0wMDAwMDAwMDAwMDEifQ.JND8PsYw1fd_MfyMrxrufWfMrJka7cD5DVaLeNKuwXlmSWjYUm6NXj70ULTr2eOTiNSmDf-S2n_llfQx3ZkbEck9brpASzMgz-C7jUjxLB1jxEncqqjFbbM84ynt0btkLy4ZLvCDvQqrgNs1MHdz2DNg1OPrZx0kMp_RIeYvX3opM0PKPv5H0w_n-5iYuHx5SDcc0a_S_qHtU2zZSETNrdqe_i-6aCwFP6JO8OZvKVS2P_w7cF0uQUTpaCXF18VhfKeD1DB2OSG4L0HSS1aynXpZprmuKjFyFJIpFuD6zZKo1MNGBgIFufuWRc8iwsHrebWkyua5eACe36qL_vCVlg \ No newline at end of file diff --git a/src/integration-test/resources/postgres/data.sql b/src/integration-test/resources/postgres/data.sql new file mode 100644 index 0000000..93d0123 --- /dev/null +++ b/src/integration-test/resources/postgres/data.sql @@ -0,0 +1,17 @@ +-- 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'), + ('00000000-0000-0000-0001-000000000002'::uuid, '2000-01-01 00:00:00.000001 +00:00', 'Thing 2'), + ('00000000-0000-0000-0001-000000000003'::uuid, '2000-01-01 00:00:00.000001 +00:00', 'Thing 3'), + ('00000000-0000-0000-0001-000000000004'::uuid, '2000-01-01 00:00:00.000001 +00:00', 'Thing 4'); + +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'); diff --git a/src/integration-test/resources/postgres/reset.sql b/src/integration-test/resources/postgres/reset.sql new file mode 100644 index 0000000..ada530d --- /dev/null +++ b/src/integration-test/resources/postgres/reset.sql @@ -0,0 +1,8 @@ +-- Disable triggers on the tables + +-- Truncate tables +TRUNCATE TABLE types; +TRUNCATE TABLE devices; +TRUNCATE TABLE nodes; + +-- Enable triggers on the account table diff --git a/src/integration-test/resources/postgres/schema.sql b/src/integration-test/resources/postgres/schema.sql new file mode 100644 index 0000000..122ae3e --- /dev/null +++ b/src/integration-test/resources/postgres/schema.sql @@ -0,0 +1,64 @@ +-- 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) +); + +-- 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 INDEX IF NOT EXISTS i_nodes_device ON public.nodes (device); diff --git a/src/test/kotlin/ltd/hlaeja/ApplicationTests.kt b/src/test/kotlin/ltd/hlaeja/ApplicationTests.kt deleted file mode 100644 index a320ae0..0000000 --- a/src/test/kotlin/ltd/hlaeja/ApplicationTests.kt +++ /dev/null @@ -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 - } -}