From a4cdfb41ecbbd5731b5c5389a6a40379cdbefced Mon Sep 17 00:00:00 2001 From: Dominik Basner Date: Fri, 24 Oct 2025 14:10:06 +0200 Subject: [PATCH 1/5] fixing inconsistency for data = null in device registry table: adding null handling and tests --- services/base-jdbc/pom.xml | 6 +- .../jdbc/store/device/TableAdapterStore.java | 36 ++- .../base/jdbc/store/StatementTest.java | 25 +- .../store/device/TableAdapterStoreTest.java | 228 ++++++++++++++++++ 4 files changed, 262 insertions(+), 33 deletions(-) create mode 100644 services/base-jdbc/src/test/java/org/eclipse/hono/service/base/jdbc/store/device/TableAdapterStoreTest.java diff --git a/services/base-jdbc/pom.xml b/services/base-jdbc/pom.xml index 8d871a3e5c..2151427fda 100644 --- a/services/base-jdbc/pom.xml +++ b/services/base-jdbc/pom.xml @@ -112,7 +112,11 @@ truth test - + + org.eclipse.hono + core-test-utils + test + diff --git a/services/base-jdbc/src/main/java/org/eclipse/hono/service/base/jdbc/store/device/TableAdapterStore.java b/services/base-jdbc/src/main/java/org/eclipse/hono/service/base/jdbc/store/device/TableAdapterStore.java index 174f38a382..6b7dad4bce 100644 --- a/services/base-jdbc/src/main/java/org/eclipse/hono/service/base/jdbc/store/device/TableAdapterStore.java +++ b/services/base-jdbc/src/main/java/org/eclipse/hono/service/base/jdbc/store/device/TableAdapterStore.java @@ -103,27 +103,25 @@ public Future> readDevice(final DeviceKey key, final .withTag(TracingHelper.TAG_DEVICE_ID, key.getDeviceId()) .start(); - return this.client.getConnection().compose(connection -> { - return readDevice(connection, key, span) - - .>flatMap(r -> { - final var entries = r.getRows(true); - switch (entries.size()) { - case 0: - return Future.succeededFuture(Optional.empty()); - case 1: - final var entry = entries.get(0); - final var device = Json.decodeValue(entry.getString("data"), Device.class); - final var version = Optional.ofNullable(entry.getString("version")); - return Future.succeededFuture(Optional.of(new DeviceReadResult(device, version))); - default: + return this.client.getConnection().compose(connection -> readDevice(connection, key, span) + .> flatMap(r -> { + final var entries = r.getRows(true); + switch (entries.size()) { + case 0: + return Future.succeededFuture(Optional.empty()); + case 1: + final var entry = entries.get(0); + final var deviceJson = entry.getString("data") != null ? entry.getString("data") : "{}"; + final var device = Json.decodeValue(deviceJson, Device.class); + final var version = Optional.ofNullable(entry.getString("version")); + return Future.succeededFuture(Optional.of(new DeviceReadResult(device, version))); + default: return Future.failedFuture(new IllegalStateException("Found multiple entries for a single device")); - } - }) + } + }) - .onComplete(x -> connection.close()) - .onComplete(x -> span.finish()); - }); + .onComplete(x -> connection.close()) + .onComplete(x -> span.finish())); } diff --git a/services/base-jdbc/src/test/java/org/eclipse/hono/service/base/jdbc/store/StatementTest.java b/services/base-jdbc/src/test/java/org/eclipse/hono/service/base/jdbc/store/StatementTest.java index e2ffc6fce2..5df39856f5 100644 --- a/services/base-jdbc/src/test/java/org/eclipse/hono/service/base/jdbc/store/StatementTest.java +++ b/services/base-jdbc/src/test/java/org/eclipse/hono/service/base/jdbc/store/StatementTest.java @@ -49,9 +49,7 @@ public void testValidateFields() { @Test public void testValidateMissingField() { final Statement statement = Statement.statement("SELECT foo FROM bar WHERE baz=:baz"); - assertThrows(IllegalStateException.class, () -> { - statement.validateParameters("bar"); - }); + assertThrows(IllegalStateException.class, () -> statement.validateParameters("bar")); } /** @@ -68,14 +66,15 @@ public void testValidateAdditionalField() { */ @Test public void testValidateAdditionalField2() { - final Statement statement = Statement.statement("UPDATE devices\n" + - "SET\n" + - " data=:data::jsonb,\n" + - " version=:next_version\n" + - "WHERE\n" + - " tenant_id=:tenant_id\n" + - "AND\n" + - " device_id=:device_id"); + final Statement statement = Statement.statement(""" + UPDATE devices + SET + data=:data::jsonb, + version=:next_version + WHERE + tenant_id=:tenant_id + AND + device_id=:device_id"""); statement.validateParameters("data", "device_id", "next_version", "tenant_id"); } @@ -102,7 +101,7 @@ public void testPlainYamlStillWorksForStatementConfig() { @Test public void testObjectCreationRejectedMapValue(@TempDir final Path tempDir) { final Path markerFile = tempDir.resolve("testObjectCreationRejectedMapValue.marker"); - final String yaml = "read: !!java.io.FileOutputStream [" + markerFile.toAbsolutePath().toString() + "]"; + final String yaml = "read: !!java.io.FileOutputStream [" + markerFile.toAbsolutePath() + "]"; assertNoMarkerFile(markerFile, yaml); } @@ -115,7 +114,7 @@ public void testObjectCreationRejectedMapValue(@TempDir final Path tempDir) { @Test public void testObjectCreationRejectedPlainValue(@TempDir final Path tempDir) { final Path markerFile = tempDir.resolve("testObjectCreationRejectedPlainValue.marker"); - final String yaml = "!!java.io.FileOutputStream [" + markerFile.toAbsolutePath().toString() + "]"; + final String yaml = "!!java.io.FileOutputStream [" + markerFile.toAbsolutePath() + "]"; assertNoMarkerFile(markerFile, yaml); } diff --git a/services/base-jdbc/src/test/java/org/eclipse/hono/service/base/jdbc/store/device/TableAdapterStoreTest.java b/services/base-jdbc/src/test/java/org/eclipse/hono/service/base/jdbc/store/device/TableAdapterStoreTest.java new file mode 100644 index 0000000000..080d52cd68 --- /dev/null +++ b/services/base-jdbc/src/test/java/org/eclipse/hono/service/base/jdbc/store/device/TableAdapterStoreTest.java @@ -0,0 +1,228 @@ +package org.eclipse.hono.service.base.jdbc.store.device; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import org.eclipse.hono.deviceregistry.service.device.DeviceKey; +import org.eclipse.hono.service.base.jdbc.store.StatementConfiguration; +import org.eclipse.hono.service.management.device.Device; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.yaml.snakeyaml.Yaml; + +import io.opentracing.Span; +import io.opentracing.SpanContext; +import io.opentracing.Tracer; +import io.opentracing.tag.StringTag; +import io.vertx.core.Future; +import io.vertx.core.json.JsonObject; +import io.vertx.jdbcclient.JDBCPool; +import io.vertx.sqlclient.Row; +import io.vertx.sqlclient.RowIterator; +import io.vertx.sqlclient.RowSet; +import io.vertx.sqlclient.Tuple; + +class TableAdapterStoreTest { + + private Span span = mock(Span.class); + private SpanContext spanContext = mock(SpanContext.class); + private io.vertx.sqlclient.SqlConnection sqlConnection = mock(io.vertx.sqlclient.SqlConnection.class); + private RowSet rowSet = mock(RowSet.class); + private Row row = mock(Row.class); + private Tracer.SpanBuilder spanBuilder = mock(Tracer.SpanBuilder.class); + private Tracer tracer = mock(Tracer.class); + private JDBCPool client = mock(JDBCPool.class); + + private TableAdapterStore store; + private static final String TENANT_ID = "test-tenant"; + private static final String DEVICE_ID = "device-1"; + private static final String VERSION = "v1"; + + /** + * Creates a test device with default values. + */ + private Device createTestDevice() { + return new Device() + .setEnabled(true) + .setVia(Collections.singletonList("group1")); + } + + /** + * Mocks a single row of device data. + * + * @param deviceJson The JSON string of the device data (can be null) + * @param version The version string + */ + private void mockSingleRow(String deviceJson, String version) { + // Setup row data + when(row.size()).thenReturn(2); + when(row.getValue(0)).thenReturn(deviceJson); + when(row.getValue(1)).thenReturn(version); + + // Setup row set + when(rowSet.columnsNames()).thenReturn(List.of("data", "version")); + when(rowSet.size()).thenReturn(1); + + // Setup iterator + var iterator = mock(RowIterator.class); + when(rowSet.iterator()).thenReturn(iterator); + when(iterator.hasNext()).thenReturn(true).thenReturn(false); + when(iterator.next()).thenReturn(row); + + // Mock forEach + doAnswer(invocation -> { + Consumer consumer = invocation.getArgument(0); + consumer.accept(row); + return null; + }).when(rowSet).forEach(any()); + + // Mock prepared query + var preparedQueryMock = mock(io.vertx.sqlclient.PreparedQuery.class); + when(preparedQueryMock.execute(any(Tuple.class))).thenReturn(Future.succeededFuture(rowSet)); + when(sqlConnection.preparedQuery(anyString())).thenReturn(preparedQueryMock); + + // Mock direct query + doAnswer(invocation -> { + io.vertx.core.Handler>> handler = invocation.getArgument(0); + handler.handle(Future.succeededFuture(rowSet)); + return null; + }).when(sqlConnection).query(anyString()); + } + + @BeforeEach + void setUp() { + // Setup tracing mocks + when(tracer.buildSpan(anyString())).thenReturn(spanBuilder); + when(spanBuilder.addReference(anyString(), any())).thenReturn(spanBuilder); + when(spanBuilder.withTag(anyString(), anyString())).thenReturn(spanBuilder); + when(spanBuilder.withTag(anyString(), any(Number.class))).thenReturn(spanBuilder); + when(spanBuilder.withTag(any(StringTag.class), anyString())).thenReturn(spanBuilder); + when(spanBuilder.ignoreActiveSpan()).thenReturn(spanBuilder); + when(spanBuilder.start()).thenReturn(span); + when(span.context()).thenReturn(spanContext); + + // Setup JDBC client mocks + when(client.getConnection()).thenReturn(Future.succeededFuture(sqlConnection)); + when(sqlConnection.close()).thenReturn(Future.succeededFuture()); + var statementConfig = StatementConfiguration.empty().overrideWith(getStatementsAsInputStream(), true); + + store = new TableAdapterStore(client, tracer, statementConfig, null); + } + + private static @NotNull ByteArrayInputStream getStatementsAsInputStream() { + Map statements = new HashMap<>(); + statements.put("readRegistration", "SELECT * FROM devices WHERE tenant_id = :tenant_id AND device_id = :device_id"); + statements.put("findCredentials", "SELECT * FROM credentials WHERE tenant_id = :tenant_id AND type = :type AND auth_id = :auth_id"); + statements.put("resolveGroups", "SELECT * FROM device_groups WHERE tenant_id = :tenant_id AND group_id = ANY(:group_ids)"); + + Yaml yaml = new Yaml(); + String yamlString = yaml.dump(statements); + return new ByteArrayInputStream(yamlString.getBytes(StandardCharsets.UTF_8)); + } + + @Test + void testReadDeviceSuccess() { + // Given + var device = createTestDevice(); + var deviceJson = JsonObject.mapFrom(device).encode(); + mockSingleRow(deviceJson, VERSION); + + // When + var result = store.readDevice(DeviceKey.from(TENANT_ID, DEVICE_ID), spanContext).result(); + + // Then + assertTrue(result.isPresent()); + var deviceResult = result.get(); + assertTrue(deviceResult.getDevice().isEnabled()); + assertEquals(VERSION, deviceResult.getResourceVersion().orElse("")); + } + + @Test + void testReadDeviceSuccess_dataNull() { + // Given + mockSingleRow(null, VERSION); + + // When + var result = store.readDevice(DeviceKey.from(TENANT_ID, DEVICE_ID), spanContext).result(); + + // Then + assertTrue(result.isPresent()); + var deviceResult = result.get(); + assertTrue(deviceResult.getDevice().isEnabled()); + assertEquals(VERSION, deviceResult.getResourceVersion().orElse("")); + } + + @Test + void testReadDeviceNotFound() { + // Given + var key = DeviceKey.from(TENANT_ID, "non-existent-device"); + + // Mock empty result set + when(rowSet.iterator()).thenReturn(mock(RowIterator.class)); + when(rowSet.size()).thenReturn(0); + doAnswer(invocation -> null).when(rowSet).forEach(any()); + + // Mock queries + var preparedQueryMock = mock(io.vertx.sqlclient.PreparedQuery.class); + when(preparedQueryMock.execute(any(Tuple.class))).thenReturn(Future.succeededFuture(rowSet)); + when(sqlConnection.preparedQuery(anyString())).thenReturn(preparedQueryMock); + + // When + var result = store.readDevice(key, spanContext).result(); + + // Then + assertTrue(result.isEmpty()); + } + + @Test + void testReadDeviceMultipleEntries() { + // Given + var device = createTestDevice(); + var deviceJson = JsonObject.mapFrom(device).encode(); + + // Mock multiple rows + when(row.size()).thenReturn(2); + when(row.getValue(0)).thenReturn(deviceJson); + when(row.getValue(1)).thenReturn(VERSION); + when(rowSet.columnsNames()).thenReturn(List.of("data", "version")); + when(rowSet.size()).thenReturn(2); + + // Setup iterator for multiple rows + var iterator = mock(RowIterator.class); + when(rowSet.iterator()).thenReturn(iterator); + when(iterator.hasNext()).thenReturn(true, true, false); + when(iterator.next()).thenReturn(row); + + // Mock forEach for multiple rows + doAnswer(invocation -> { + Consumer consumer = invocation.getArgument(0); + consumer.accept(row); + consumer.accept(row); + return null; + }).when(rowSet).forEach(any()); + + // Mock queries + var preparedQueryMock = mock(io.vertx.sqlclient.PreparedQuery.class); + when(preparedQueryMock.execute(any(Tuple.class))).thenReturn(Future.succeededFuture(rowSet)); + when(sqlConnection.preparedQuery(anyString())).thenReturn(preparedQueryMock); + + // When/Then + var future = store.readDevice(DeviceKey.from(TENANT_ID, "duplicate-device"), spanContext); + assertTrue(future.failed()); + assertEquals("Found multiple entries for a single device", future.cause().getMessage()); + } +} \ No newline at end of file From ca9aff601855e1e3761fde4dfe83422389639aaa Mon Sep 17 00:00:00 2001 From: Dominik Basner Date: Fri, 24 Oct 2025 14:14:09 +0200 Subject: [PATCH 2/5] adding comments and copyright header to TableAdapterStoreTest --- .../store/device/TableAdapterStoreTest.java | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/services/base-jdbc/src/test/java/org/eclipse/hono/service/base/jdbc/store/device/TableAdapterStoreTest.java b/services/base-jdbc/src/test/java/org/eclipse/hono/service/base/jdbc/store/device/TableAdapterStoreTest.java index 080d52cd68..accae7fae5 100644 --- a/services/base-jdbc/src/test/java/org/eclipse/hono/service/base/jdbc/store/device/TableAdapterStoreTest.java +++ b/services/base-jdbc/src/test/java/org/eclipse/hono/service/base/jdbc/store/device/TableAdapterStoreTest.java @@ -1,3 +1,16 @@ +/******************************************************************************* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ + package org.eclipse.hono.service.base.jdbc.store.device; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -36,16 +49,19 @@ import io.vertx.sqlclient.RowSet; import io.vertx.sqlclient.Tuple; +/** + * Tests the handling of device data in the database. + */ class TableAdapterStoreTest { - private Span span = mock(Span.class); - private SpanContext spanContext = mock(SpanContext.class); - private io.vertx.sqlclient.SqlConnection sqlConnection = mock(io.vertx.sqlclient.SqlConnection.class); - private RowSet rowSet = mock(RowSet.class); - private Row row = mock(Row.class); - private Tracer.SpanBuilder spanBuilder = mock(Tracer.SpanBuilder.class); - private Tracer tracer = mock(Tracer.class); - private JDBCPool client = mock(JDBCPool.class); + private final Span span = mock(Span.class); + private final SpanContext spanContext = mock(SpanContext.class); + private final io.vertx.sqlclient.SqlConnection sqlConnection = mock(io.vertx.sqlclient.SqlConnection.class); + private final RowSet rowSet = mock(RowSet.class); + private final Row row = mock(Row.class); + private final Tracer.SpanBuilder spanBuilder = mock(Tracer.SpanBuilder.class); + private final Tracer tracer = mock(Tracer.class); + private final JDBCPool client = mock(JDBCPool.class); private TableAdapterStore store; private static final String TENANT_ID = "test-tenant"; @@ -67,7 +83,7 @@ private Device createTestDevice() { * @param deviceJson The JSON string of the device data (can be null) * @param version The version string */ - private void mockSingleRow(String deviceJson, String version) { + private void mockSingleRow(final String deviceJson, final String version) { // Setup row data when(row.size()).thenReturn(2); when(row.getValue(0)).thenReturn(deviceJson); From 1a664923bcfe29f0abbb7d76deaf97cbb63bbc3e Mon Sep 17 00:00:00 2001 From: Dominik Basner Date: Fri, 24 Oct 2025 15:13:56 +0200 Subject: [PATCH 3/5] fix checkstyle issues --- .../store/device/TableAdapterStoreTest.java | 99 ++++++++++--------- 1 file changed, 51 insertions(+), 48 deletions(-) diff --git a/services/base-jdbc/src/test/java/org/eclipse/hono/service/base/jdbc/store/device/TableAdapterStoreTest.java b/services/base-jdbc/src/test/java/org/eclipse/hono/service/base/jdbc/store/device/TableAdapterStoreTest.java index accae7fae5..532d2a2f9a 100644 --- a/services/base-jdbc/src/test/java/org/eclipse/hono/service/base/jdbc/store/device/TableAdapterStoreTest.java +++ b/services/base-jdbc/src/test/java/org/eclipse/hono/service/base/jdbc/store/device/TableAdapterStoreTest.java @@ -64,9 +64,9 @@ class TableAdapterStoreTest { private final JDBCPool client = mock(JDBCPool.class); private TableAdapterStore store; - private static final String TENANT_ID = "test-tenant"; - private static final String DEVICE_ID = "device-1"; - private static final String VERSION = "v1"; + private final String TENANT_ID = "test-tenant"; + private final String DEVICE_ID = "device-1"; + private final String VERSION = "v1"; /** * Creates a test device with default values. @@ -88,32 +88,32 @@ private void mockSingleRow(final String deviceJson, final String version) { when(row.size()).thenReturn(2); when(row.getValue(0)).thenReturn(deviceJson); when(row.getValue(1)).thenReturn(version); - + // Setup row set when(rowSet.columnsNames()).thenReturn(List.of("data", "version")); when(rowSet.size()).thenReturn(1); - + // Setup iterator - var iterator = mock(RowIterator.class); + final var iterator = mock(RowIterator.class); when(rowSet.iterator()).thenReturn(iterator); when(iterator.hasNext()).thenReturn(true).thenReturn(false); when(iterator.next()).thenReturn(row); - + // Mock forEach doAnswer(invocation -> { - Consumer consumer = invocation.getArgument(0); + final Consumer consumer = invocation.getArgument(0); consumer.accept(row); return null; }).when(rowSet).forEach(any()); - + // Mock prepared query - var preparedQueryMock = mock(io.vertx.sqlclient.PreparedQuery.class); + final var preparedQueryMock = mock(io.vertx.sqlclient.PreparedQuery.class); when(preparedQueryMock.execute(any(Tuple.class))).thenReturn(Future.succeededFuture(rowSet)); when(sqlConnection.preparedQuery(anyString())).thenReturn(preparedQueryMock); - + // Mock direct query doAnswer(invocation -> { - io.vertx.core.Handler>> handler = invocation.getArgument(0); + final io.vertx.core.Handler>> handler = invocation.getArgument(0); handler.handle(Future.succeededFuture(rowSet)); return null; }).when(sqlConnection).query(anyString()); @@ -130,39 +130,42 @@ void setUp() { when(spanBuilder.ignoreActiveSpan()).thenReturn(spanBuilder); when(spanBuilder.start()).thenReturn(span); when(span.context()).thenReturn(spanContext); - + // Setup JDBC client mocks when(client.getConnection()).thenReturn(Future.succeededFuture(sqlConnection)); when(sqlConnection.close()).thenReturn(Future.succeededFuture()); - var statementConfig = StatementConfiguration.empty().overrideWith(getStatementsAsInputStream(), true); + final var statementConfig = StatementConfiguration.empty().overrideWith(getStatementsAsInputStream(), true); store = new TableAdapterStore(client, tracer, statementConfig, null); } private static @NotNull ByteArrayInputStream getStatementsAsInputStream() { - Map statements = new HashMap<>(); - statements.put("readRegistration", "SELECT * FROM devices WHERE tenant_id = :tenant_id AND device_id = :device_id"); - statements.put("findCredentials", "SELECT * FROM credentials WHERE tenant_id = :tenant_id AND type = :type AND auth_id = :auth_id"); - statements.put("resolveGroups", "SELECT * FROM device_groups WHERE tenant_id = :tenant_id AND group_id = ANY(:group_ids)"); - - Yaml yaml = new Yaml(); - String yamlString = yaml.dump(statements); + final Map statements = new HashMap<>(); + statements.put("readRegistration", + "SELECT * FROM devices WHERE tenant_id = :tenant_id AND device_id = :device_id"); + statements.put("findCredentials", + "SELECT * FROM credentials WHERE tenant_id = :tenant_id AND type = :type AND auth_id = :auth_id"); + statements.put("resolveGroups", + "SELECT * FROM device_groups WHERE tenant_id = :tenant_id AND group_id = ANY(:group_ids)"); + + final Yaml yaml = new Yaml(); + final String yamlString = yaml.dump(statements); return new ByteArrayInputStream(yamlString.getBytes(StandardCharsets.UTF_8)); } @Test void testReadDeviceSuccess() { // Given - var device = createTestDevice(); - var deviceJson = JsonObject.mapFrom(device).encode(); + final var device = createTestDevice(); + final var deviceJson = JsonObject.mapFrom(device).encode(); mockSingleRow(deviceJson, VERSION); // When - var result = store.readDevice(DeviceKey.from(TENANT_ID, DEVICE_ID), spanContext).result(); + final var result = store.readDevice(DeviceKey.from(TENANT_ID, DEVICE_ID), spanContext).result(); // Then assertTrue(result.isPresent()); - var deviceResult = result.get(); + final var deviceResult = result.get(); assertTrue(deviceResult.getDevice().isEnabled()); assertEquals(VERSION, deviceResult.getResourceVersion().orElse("")); } @@ -171,13 +174,13 @@ void testReadDeviceSuccess() { void testReadDeviceSuccess_dataNull() { // Given mockSingleRow(null, VERSION); - + // When - var result = store.readDevice(DeviceKey.from(TENANT_ID, DEVICE_ID), spanContext).result(); - + final var result = store.readDevice(DeviceKey.from(TENANT_ID, DEVICE_ID), spanContext).result(); + // Then assertTrue(result.isPresent()); - var deviceResult = result.get(); + final var deviceResult = result.get(); assertTrue(deviceResult.getDevice().isEnabled()); assertEquals(VERSION, deviceResult.getResourceVersion().orElse("")); } @@ -185,21 +188,21 @@ void testReadDeviceSuccess_dataNull() { @Test void testReadDeviceNotFound() { // Given - var key = DeviceKey.from(TENANT_ID, "non-existent-device"); - + final var key = DeviceKey.from(TENANT_ID, "non-existent-device"); + // Mock empty result set when(rowSet.iterator()).thenReturn(mock(RowIterator.class)); when(rowSet.size()).thenReturn(0); doAnswer(invocation -> null).when(rowSet).forEach(any()); - + // Mock queries - var preparedQueryMock = mock(io.vertx.sqlclient.PreparedQuery.class); + final var preparedQueryMock = mock(io.vertx.sqlclient.PreparedQuery.class); when(preparedQueryMock.execute(any(Tuple.class))).thenReturn(Future.succeededFuture(rowSet)); when(sqlConnection.preparedQuery(anyString())).thenReturn(preparedQueryMock); - + // When - var result = store.readDevice(key, spanContext).result(); - + final var result = store.readDevice(key, spanContext).result(); + // Then assertTrue(result.isEmpty()); } @@ -207,38 +210,38 @@ void testReadDeviceNotFound() { @Test void testReadDeviceMultipleEntries() { // Given - var device = createTestDevice(); - var deviceJson = JsonObject.mapFrom(device).encode(); - + final var device = createTestDevice(); + final var deviceJson = JsonObject.mapFrom(device).encode(); + // Mock multiple rows when(row.size()).thenReturn(2); when(row.getValue(0)).thenReturn(deviceJson); when(row.getValue(1)).thenReturn(VERSION); when(rowSet.columnsNames()).thenReturn(List.of("data", "version")); when(rowSet.size()).thenReturn(2); - + // Setup iterator for multiple rows - var iterator = mock(RowIterator.class); + final var iterator = mock(RowIterator.class); when(rowSet.iterator()).thenReturn(iterator); when(iterator.hasNext()).thenReturn(true, true, false); when(iterator.next()).thenReturn(row); - + // Mock forEach for multiple rows doAnswer(invocation -> { - Consumer consumer = invocation.getArgument(0); + final Consumer consumer = invocation.getArgument(0); consumer.accept(row); consumer.accept(row); return null; }).when(rowSet).forEach(any()); - + // Mock queries - var preparedQueryMock = mock(io.vertx.sqlclient.PreparedQuery.class); + final var preparedQueryMock = mock(io.vertx.sqlclient.PreparedQuery.class); when(preparedQueryMock.execute(any(Tuple.class))).thenReturn(Future.succeededFuture(rowSet)); when(sqlConnection.preparedQuery(anyString())).thenReturn(preparedQueryMock); - + // When/Then - var future = store.readDevice(DeviceKey.from(TENANT_ID, "duplicate-device"), spanContext); + final var future = store.readDevice(DeviceKey.from(TENANT_ID, "duplicate-device"), spanContext); assertTrue(future.failed()); assertEquals("Found multiple entries for a single device", future.cause().getMessage()); } -} \ No newline at end of file +} From 32cfb5516fe8613e543e50cc774616889fb71c7d Mon Sep 17 00:00:00 2001 From: Dominik Basner Date: Fri, 12 Dec 2025 10:23:00 +0100 Subject: [PATCH 4/5] Updating indent in pom.xml and TableAdapterStoreTest.java --- services/base-jdbc/pom.xml | 10 ++++----- .../store/device/TableAdapterStoreTest.java | 22 +++++++++++-------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/services/base-jdbc/pom.xml b/services/base-jdbc/pom.xml index 2151427fda..924436c80a 100644 --- a/services/base-jdbc/pom.xml +++ b/services/base-jdbc/pom.xml @@ -112,11 +112,11 @@ truth test - - org.eclipse.hono - core-test-utils - test - + + org.eclipse.hono + core-test-utils + test + diff --git a/services/base-jdbc/src/test/java/org/eclipse/hono/service/base/jdbc/store/device/TableAdapterStoreTest.java b/services/base-jdbc/src/test/java/org/eclipse/hono/service/base/jdbc/store/device/TableAdapterStoreTest.java index 532d2a2f9a..e490c70ba5 100644 --- a/services/base-jdbc/src/test/java/org/eclipse/hono/service/base/jdbc/store/device/TableAdapterStoreTest.java +++ b/services/base-jdbc/src/test/java/org/eclipse/hono/service/base/jdbc/store/device/TableAdapterStoreTest.java @@ -27,6 +27,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.Consumer; import org.eclipse.hono.deviceregistry.service.device.DeviceKey; @@ -66,7 +67,6 @@ class TableAdapterStoreTest { private TableAdapterStore store; private final String TENANT_ID = "test-tenant"; private final String DEVICE_ID = "device-1"; - private final String VERSION = "v1"; /** * Creates a test device with default values. @@ -156,33 +156,36 @@ void setUp() { @Test void testReadDeviceSuccess() { // Given + final var version = "v1"; final var device = createTestDevice(); final var deviceJson = JsonObject.mapFrom(device).encode(); - mockSingleRow(deviceJson, VERSION); + mockSingleRow(deviceJson, version); // When final var result = store.readDevice(DeviceKey.from(TENANT_ID, DEVICE_ID), spanContext).result(); // Then - assertTrue(result.isPresent()); - final var deviceResult = result.get(); - assertTrue(deviceResult.getDevice().isEnabled()); - assertEquals(VERSION, deviceResult.getResourceVersion().orElse("")); + validateReadDeviceResult(result, version); } @Test void testReadDeviceSuccess_dataNull() { // Given - mockSingleRow(null, VERSION); + final var version = "v2"; + mockSingleRow(null, version); // When final var result = store.readDevice(DeviceKey.from(TENANT_ID, DEVICE_ID), spanContext).result(); // Then + validateReadDeviceResult(result, version); + } + + private void validateReadDeviceResult(final Optional result, final String version) { assertTrue(result.isPresent()); final var deviceResult = result.get(); assertTrue(deviceResult.getDevice().isEnabled()); - assertEquals(VERSION, deviceResult.getResourceVersion().orElse("")); + assertEquals(version, deviceResult.getResourceVersion().orElse("")); } @Test @@ -216,7 +219,8 @@ void testReadDeviceMultipleEntries() { // Mock multiple rows when(row.size()).thenReturn(2); when(row.getValue(0)).thenReturn(deviceJson); - when(row.getValue(1)).thenReturn(VERSION); + final var version = "v3"; + when(row.getValue(1)).thenReturn(version); when(rowSet.columnsNames()).thenReturn(List.of("data", "version")); when(rowSet.size()).thenReturn(2); From 5214878662b611e3ce5dc933d88a4ca4c61619b5 Mon Sep 17 00:00:00 2001 From: Dominik Basner Date: Fri, 12 Dec 2025 10:58:51 +0100 Subject: [PATCH 5/5] putting test rules into methods --- .../store/device/TableAdapterStoreTest.java | 110 +++++++----------- 1 file changed, 42 insertions(+), 68 deletions(-) diff --git a/services/base-jdbc/src/test/java/org/eclipse/hono/service/base/jdbc/store/device/TableAdapterStoreTest.java b/services/base-jdbc/src/test/java/org/eclipse/hono/service/base/jdbc/store/device/TableAdapterStoreTest.java index e490c70ba5..3c8912fb7d 100644 --- a/services/base-jdbc/src/test/java/org/eclipse/hono/service/base/jdbc/store/device/TableAdapterStoreTest.java +++ b/services/base-jdbc/src/test/java/org/eclipse/hono/service/base/jdbc/store/device/TableAdapterStoreTest.java @@ -64,6 +64,7 @@ class TableAdapterStoreTest { private final Tracer tracer = mock(Tracer.class); private final JDBCPool client = mock(JDBCPool.class); + private String deviceJson; private TableAdapterStore store; private final String TENANT_ID = "test-tenant"; private final String DEVICE_ID = "device-1"; @@ -81,42 +82,48 @@ private Device createTestDevice() { * Mocks a single row of device data. * * @param deviceJson The JSON string of the device data (can be null) - * @param version The version string + * @param versions The versions the mocked rows contain */ - private void mockSingleRow(final String deviceJson, final String version) { + private void mockRows(final String deviceJson, final String[] versions) { // Setup row data - when(row.size()).thenReturn(2); - when(row.getValue(0)).thenReturn(deviceJson); - when(row.getValue(1)).thenReturn(version); + for (String version : versions) { + when(row.size()).thenReturn(2); + when(row.getValue(0)).thenReturn(deviceJson); + when(row.getValue(1)).thenReturn(version); + } - // Setup row set - when(rowSet.columnsNames()).thenReturn(List.of("data", "version")); - when(rowSet.size()).thenReturn(1); + mockRowSet(versions.length); - // Setup iterator + // Mock direct query + doAnswer(invocation -> { + final io.vertx.core.Handler>> handler = invocation.getArgument(0); + handler.handle(Future.succeededFuture(rowSet)); + return null; + }).when(sqlConnection).query(anyString()); + } + + private void mockRowSet(final int numRows) { final var iterator = mock(RowIterator.class); when(rowSet.iterator()).thenReturn(iterator); - when(iterator.hasNext()).thenReturn(true).thenReturn(false); - when(iterator.next()).thenReturn(row); - - // Mock forEach + when(rowSet.columnsNames()).thenReturn(List.of("data", "version")); + when(rowSet.size()).thenReturn(numRows); doAnswer(invocation -> { final Consumer consumer = invocation.getArgument(0); - consumer.accept(row); + for (int i = 0; i < numRows; i++) { + consumer.accept(row); + } return null; }).when(rowSet).forEach(any()); + for (int i = 0; i < numRows; i++) { + when(iterator.hasNext()).thenReturn(true); + } + when(iterator.hasNext()).thenReturn(false); + when(iterator.next()).thenReturn(row); - // Mock prepared query + // Return rowSet in query: final var preparedQueryMock = mock(io.vertx.sqlclient.PreparedQuery.class); when(preparedQueryMock.execute(any(Tuple.class))).thenReturn(Future.succeededFuture(rowSet)); when(sqlConnection.preparedQuery(anyString())).thenReturn(preparedQueryMock); - - // Mock direct query - doAnswer(invocation -> { - final io.vertx.core.Handler>> handler = invocation.getArgument(0); - handler.handle(Future.succeededFuture(rowSet)); - return null; - }).when(sqlConnection).query(anyString()); } @BeforeEach @@ -137,6 +144,8 @@ void setUp() { final var statementConfig = StatementConfiguration.empty().overrideWith(getStatementsAsInputStream(), true); store = new TableAdapterStore(client, tracer, statementConfig, null); + final var device = createTestDevice(); + deviceJson = JsonObject.mapFrom(device).encode(); } private static @NotNull ByteArrayInputStream getStatementsAsInputStream() { @@ -157,9 +166,7 @@ void setUp() { void testReadDeviceSuccess() { // Given final var version = "v1"; - final var device = createTestDevice(); - final var deviceJson = JsonObject.mapFrom(device).encode(); - mockSingleRow(deviceJson, version); + mockRows(deviceJson, new String[]{version}); // When final var result = store.readDevice(DeviceKey.from(TENANT_ID, DEVICE_ID), spanContext).result(); @@ -172,7 +179,7 @@ void testReadDeviceSuccess() { void testReadDeviceSuccess_dataNull() { // Given final var version = "v2"; - mockSingleRow(null, version); + mockRows(null, new String[]{version}); // When final var result = store.readDevice(DeviceKey.from(TENANT_ID, DEVICE_ID), spanContext).result(); @@ -192,59 +199,26 @@ private void validateReadDeviceResult(final Optional result, f void testReadDeviceNotFound() { // Given final var key = DeviceKey.from(TENANT_ID, "non-existent-device"); - - // Mock empty result set - when(rowSet.iterator()).thenReturn(mock(RowIterator.class)); - when(rowSet.size()).thenReturn(0); - doAnswer(invocation -> null).when(rowSet).forEach(any()); - - // Mock queries - final var preparedQueryMock = mock(io.vertx.sqlclient.PreparedQuery.class); - when(preparedQueryMock.execute(any(Tuple.class))).thenReturn(Future.succeededFuture(rowSet)); - when(sqlConnection.preparedQuery(anyString())).thenReturn(preparedQueryMock); + mockRows(deviceJson, new String[]{}); // When - final var result = store.readDevice(key, spanContext).result(); + final var future = store.readDevice(key, spanContext); // Then - assertTrue(result.isEmpty()); + assertTrue(future.succeeded()); + assertTrue(future.result().isEmpty()); } @Test void testReadDeviceMultipleEntries() { // Given - final var device = createTestDevice(); - final var deviceJson = JsonObject.mapFrom(device).encode(); - - // Mock multiple rows - when(row.size()).thenReturn(2); - when(row.getValue(0)).thenReturn(deviceJson); - final var version = "v3"; - when(row.getValue(1)).thenReturn(version); - when(rowSet.columnsNames()).thenReturn(List.of("data", "version")); - when(rowSet.size()).thenReturn(2); - - // Setup iterator for multiple rows - final var iterator = mock(RowIterator.class); - when(rowSet.iterator()).thenReturn(iterator); - when(iterator.hasNext()).thenReturn(true, true, false); - when(iterator.next()).thenReturn(row); - - // Mock forEach for multiple rows - doAnswer(invocation -> { - final Consumer consumer = invocation.getArgument(0); - consumer.accept(row); - consumer.accept(row); - return null; - }).when(rowSet).forEach(any()); + final var key = DeviceKey.from(TENANT_ID, "duplicate-device"); + mockRows(deviceJson, new String[]{"v3", "v4"}); - // Mock queries - final var preparedQueryMock = mock(io.vertx.sqlclient.PreparedQuery.class); - when(preparedQueryMock.execute(any(Tuple.class))).thenReturn(Future.succeededFuture(rowSet)); - when(sqlConnection.preparedQuery(anyString())).thenReturn(preparedQueryMock); + // When + final var future = store.readDevice(key, spanContext); - // When/Then - final var future = store.readDevice(DeviceKey.from(TENANT_ID, "duplicate-device"), spanContext); + // Then assertTrue(future.failed()); assertEquals("Found multiple entries for a single device", future.cause().getMessage()); }