2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.persistence.dynamodb.internal;
15 import static org.junit.jupiter.api.Assertions.*;
16 import static org.mockito.Mockito.when;
18 import java.net.ServerSocket;
20 import java.util.Collection;
21 import java.util.HashMap;
22 import java.util.Hashtable;
24 import java.util.Map.Entry;
25 import java.util.Optional;
26 import java.util.concurrent.ExecutionException;
27 import java.util.stream.Stream;
29 import javax.measure.Unit;
30 import javax.measure.quantity.Dimensionless;
31 import javax.measure.quantity.Temperature;
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.junit.jupiter.api.AfterAll;
36 import org.junit.jupiter.api.BeforeAll;
37 import org.junit.jupiter.api.TestInfo;
38 import org.mockito.Mockito;
39 import org.openhab.core.common.registry.RegistryChangeListener;
40 import org.openhab.core.i18n.UnitProvider;
41 import org.openhab.core.internal.i18n.I18nProviderImpl;
42 import org.openhab.core.items.GroupItem;
43 import org.openhab.core.items.Item;
44 import org.openhab.core.items.ItemNotFoundException;
45 import org.openhab.core.items.ItemNotUniqueException;
46 import org.openhab.core.items.ItemRegistry;
47 import org.openhab.core.items.RegistryHook;
48 import org.openhab.core.library.items.CallItem;
49 import org.openhab.core.library.items.ColorItem;
50 import org.openhab.core.library.items.ContactItem;
51 import org.openhab.core.library.items.DateTimeItem;
52 import org.openhab.core.library.items.DimmerItem;
53 import org.openhab.core.library.items.LocationItem;
54 import org.openhab.core.library.items.NumberItem;
55 import org.openhab.core.library.items.PlayerItem;
56 import org.openhab.core.library.items.RollershutterItem;
57 import org.openhab.core.library.items.StringItem;
58 import org.openhab.core.library.items.SwitchItem;
59 import org.openhab.core.library.unit.SIUnits;
60 import org.openhab.core.library.unit.Units;
61 import org.openhab.core.test.java.JavaTest;
62 import org.osgi.framework.BundleContext;
63 import org.osgi.service.component.ComponentContext;
64 import org.slf4j.Logger;
65 import org.slf4j.LoggerFactory;
67 import com.amazonaws.services.dynamodbv2.local.main.ServerRunner;
68 import com.amazonaws.services.dynamodbv2.local.server.DynamoDBProxyServer;
70 import software.amazon.awssdk.core.waiters.WaiterResponse;
71 import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
72 import software.amazon.awssdk.services.dynamodb.model.DescribeTableResponse;
73 import software.amazon.awssdk.services.dynamodb.model.ResourceNotFoundException;
77 * @author Sami Salonen - Initial contribution
81 public class BaseIntegrationTest extends JavaTest {
82 protected static final String TABLE = "dynamodb-integration-tests";
83 protected static final String TABLE_PREFIX = "dynamodb-integration-tests-";
84 protected static final Logger LOGGER = LoggerFactory.getLogger(DynamoDBPersistenceService.class);
85 protected static @Nullable DynamoDBPersistenceService service;
86 protected static final Map<String, Item> ITEMS = new HashMap<>();
87 protected static @Nullable DynamoDBProxyServer embeddedServer;
89 * SI system has Celsius as temperature unit
91 protected static final Unit<Temperature> TEMP_ITEM_UNIT = SIUnits.CELSIUS;
92 protected static final Unit<Dimensionless> DIMENSIONLESS_ITEM_UNIT = Units.ONE;
93 private static @Nullable URI endpointOverride;
95 protected static final UnitProvider UNIT_PROVIDER;
97 ComponentContext context = Mockito.mock(ComponentContext.class);
98 BundleContext bundleContext = Mockito.mock(BundleContext.class);
99 Hashtable<String, Object> properties = new Hashtable<String, Object>();
100 properties.put("measurementSystem", SIUnits.MEASUREMENT_SYSTEM_NAME);
101 when(context.getProperties()).thenReturn(properties);
102 when(context.getBundleContext()).thenReturn(bundleContext);
103 UNIT_PROVIDER = new I18nProviderImpl(context);
107 * Whether tests are run in Continuous Integration environment, i.e. Jenkins or Travis CI
109 * Travis CI is detected using CI environment variable, see https://docs.travis-ci.com/user/environment-variables/
110 * Jenkins CI is detected using JENKINS_HOME environment variable
114 protected static boolean isRunningInCI() {
115 String jenkinsHome = System.getenv("JENKINS_HOME");
116 return "true".equals(System.getenv("CI")) || (jenkinsHome != null && !jenkinsHome.isBlank());
119 private static boolean credentialsSet() {
120 String access = System.getProperty("DYNAMODBTEST_ACCESS");
121 String secret = System.getProperty("DYNAMODBTEST_SECRET");
122 return access != null && !access.isBlank() && secret != null && !secret.isBlank();
125 private static int findFreeTCPPort() {
126 try (ServerSocket serverSocket = new ServerSocket(0)) {
127 int localPort = serverSocket.getLocalPort();
128 assertTrue(localPort > 0);
130 } catch (Exception e) {
131 fail("Unable to find free tcp port for embedded DynamoDB server");
132 return -1; // Make compiler happy
137 protected void waitForAssert(Runnable runnable) {
138 // Longer timeouts and slower polling with real dynamodb
139 // Non-CI tests against local server are with lower timeout.
140 waitForAssert(runnable, hasFakeServer() ? isRunningInCI() ? 30_000L : 10_000L : 120_000L,
141 hasFakeServer() ? 500 : 1000L);
145 protected static void populateItems() {
146 ITEMS.put("dimmer", new DimmerItem("dimmer"));
147 ITEMS.put("number", new NumberItem("number"));
149 NumberItem temperatureItem = new NumberItem("Number:Temperature", "numberTemperature", UNIT_PROVIDER);
150 ITEMS.put("numberTemperature", temperatureItem);
151 GroupItem groupTemperature = new GroupItem("groupNumberTemperature", temperatureItem);
152 ITEMS.put("groupNumberTemperature", groupTemperature);
154 NumberItem dimensionlessItem = new NumberItem("Number:Dimensionless", "numberDimensionless", UNIT_PROVIDER);
155 ITEMS.put("numberDimensionless", dimensionlessItem);
156 GroupItem groupDimensionless = new GroupItem("groupNumberDimensionless", dimensionlessItem);
157 ITEMS.put("groupNumberDimensionless", groupDimensionless);
159 GroupItem groupDummy = new GroupItem("dummyGroup", null);
160 ITEMS.put("groupDummy", groupDummy);
162 ITEMS.put("string", new StringItem("string"));
163 ITEMS.put("switch", new SwitchItem("switch"));
164 ITEMS.put("contact", new ContactItem("contact"));
165 ITEMS.put("color", new ColorItem("color"));
166 ITEMS.put("rollershutter", new RollershutterItem("rollershutter"));
167 ITEMS.put("datetime", new DateTimeItem("datetime"));
168 ITEMS.put("call", new CallItem("call"));
169 ITEMS.put("location", new LocationItem("location"));
170 ITEMS.put("player_playpause", new PlayerItem("player_playpause"));
171 ITEMS.put("player_rewindfastforward", new PlayerItem("player_rewindfastforward"));
175 public static void initService(TestInfo testInfo) throws InterruptedException, IllegalArgumentException,
176 IllegalAccessException, NoSuchFieldException, SecurityException {
177 service = newService(isLegacyTest(testInfo), true, null, null, null);
182 * Create new persistence service. Either pointing to real DynamoDB (given credentials as java properties) or local
185 * @param legacy whether to create config that implies legacy or new schema. Use null for MAYBE_LEGACY
186 * @param cleanLocal when creating local DB, whether to create new DB
187 * @param overrideLocalURI URI to use when using local DB
190 * @return new persistence service
192 protected static synchronized DynamoDBPersistenceService newService(@Nullable Boolean legacy, boolean cleanLocal,
193 @Nullable URI overrideLocalURI, @Nullable String table, @Nullable String tablePrefix) {
194 final DynamoDBPersistenceService service;
195 Map<String, Object> config = getConfig(legacy, table, tablePrefix);
196 if (cleanLocal && overrideLocalURI != null) {
197 throw new IllegalArgumentException("cannot specify both cleanLocal=true and overrideLocalURI");
199 if (legacy == null && (table != null || tablePrefix != null)) {
200 throw new IllegalArgumentException("cannot specify both legacy=null and unambiguous table configuration");
202 URI localEndpointOverride = overrideLocalURI == null ? endpointOverride : overrideLocalURI;
203 if (overrideLocalURI == null && !credentialsSet() && (cleanLocal || endpointOverride == null)) {
204 // Local server not started yet, start it
205 // endpointOverride static field has the URI
206 LOGGER.info("Since credentials have not been defined, using embedded local AWS DynamoDB server");
207 System.setProperty("sqlite4java.library.path", "src/test/resources/native-libs");
208 int port = findFreeTCPPort();
209 String endpoint = String.format("http://127.0.0.1:%d", port);
211 localEndpointOverride = new URI(endpoint);
212 DynamoDBProxyServer localEmbeddedServer = ServerRunner
213 .createServerFromCommandLineArgs(new String[] { "-inMemory", "-port", String.valueOf(port) });
214 localEmbeddedServer.start();
215 embeddedServer = localEmbeddedServer;
216 } catch (Exception e) {
217 fail("Error with embedded DynamoDB server", e);
218 throw new IllegalStateException();
222 if (endpointOverride == null) {
223 endpointOverride = localEndpointOverride;
226 service = new DynamoDBPersistenceService(new ItemRegistry() {
228 public Collection<Item> getItems(String pattern) {
229 throw new UnsupportedOperationException();
233 public Collection<Item> getItems() {
234 throw new UnsupportedOperationException();
238 public Item getItemByPattern(String name) throws ItemNotFoundException, ItemNotUniqueException {
239 throw new UnsupportedOperationException();
243 public Item getItem(String name) throws ItemNotFoundException {
244 Item item = ITEMS.get(name);
246 throw new ItemNotFoundException(name);
252 public void addRegistryChangeListener(RegistryChangeListener<Item> listener) {
253 throw new UnsupportedOperationException();
257 public Collection<Item> getAll() {
258 throw new UnsupportedOperationException();
262 public Stream<Item> stream() {
263 throw new UnsupportedOperationException();
267 public @Nullable Item get(String key) {
268 throw new UnsupportedOperationException();
272 public void removeRegistryChangeListener(RegistryChangeListener<Item> listener) {
273 throw new UnsupportedOperationException();
277 public Item add(Item element) {
278 throw new UnsupportedOperationException();
282 public @Nullable Item update(Item element) {
283 throw new UnsupportedOperationException();
287 public @Nullable Item remove(String key) {
288 throw new UnsupportedOperationException();
292 public Collection<Item> getItemsOfType(String type) {
293 throw new UnsupportedOperationException();
297 public Collection<Item> getItemsByTag(String... tags) {
298 throw new UnsupportedOperationException();
302 public Collection<Item> getItemsByTagAndType(String type, String... tags) {
303 throw new UnsupportedOperationException();
307 public <T extends Item> Collection<T> getItemsByTag(Class<T> typeFilter, String... tags) {
308 throw new UnsupportedOperationException();
312 public @Nullable Item remove(String itemName, boolean recursive) {
313 throw new UnsupportedOperationException();
317 public void addRegistryHook(RegistryHook<Item> hook) {
318 throw new UnsupportedOperationException();
322 public void removeRegistryHook(RegistryHook<Item> hook) {
323 throw new UnsupportedOperationException();
325 }, UNIT_PROVIDER, localEndpointOverride);
327 service.activate(null, config);
331 private static Map<String, Object> getConfig(@Nullable Boolean legacy, @Nullable String table,
332 @Nullable String tablePrefix) {
333 Map<String, Object> config = new HashMap<>();
334 if (legacy != null) {
335 if (legacy.booleanValue()) {
336 LOGGER.info("Legacy test");
337 config.put("tablePrefix", tablePrefix == null ? TABLE_PREFIX : tablePrefix);
339 LOGGER.info("Non-legacy test");
340 config.put("table", table == null ? TABLE : table);
341 config.put("expireDays", "1");
345 if (credentialsSet()) {
346 LOGGER.info("Since credentials have been defined, using real AWS DynamoDB");
348 String value = System.getProperty("DYNAMODBTEST_REGION");
349 config.put("region", value != null ? value : "");
350 value = System.getProperty("DYNAMODBTEST_ACCESS");
351 config.put("accessKey", value != null ? value : "");
352 value = System.getProperty("DYNAMODBTEST_SECRET");
353 config.put("secretKey", value != null ? value : "");
355 for (Entry<String, Object> entry : config.entrySet()) {
356 if (((String) entry.getValue()).isEmpty()) {
357 fail("Expecting " + entry.getKey()
358 + " to have value for integration tests. Integration test will fail");
359 throw new IllegalArgumentException();
363 // Place some values to pass the configuration validation
364 config.put("region", "eu-west-1");
365 config.put("accessKey", "dummy-access-key");
366 config.put("secretKey", "dummy-secret-key");
371 protected static boolean isLegacyTest(TestInfo testInfo) {
373 return testInfo.getTestClass().get().getDeclaredField("LEGACY_MODE").getBoolean(null);
374 } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) {
375 fail("Could not find static boolean LEGACY_MODE from the test class: " + e.getClass().getSimpleName() + " "
377 throw new IllegalStateException(); // Making compiler happy
381 protected boolean hasFakeServer() {
382 return embeddedServer != null;
386 public static void tearDown() {
388 if (embeddedServer != null) {
389 embeddedServer.stop();
391 } catch (Exception e) {
392 fail("Error stopping embedded server", e);
396 protected static void clearData() {
397 DynamoDBPersistenceService localService = service;
398 assert localService != null;
399 DynamoDbAsyncClient lowLevelClient = localService.getLowLevelClient();
400 assertNotNull(lowLevelClient);
401 assert lowLevelClient != null;// To get rid of null exception
403 for (String table : new String[] { "dynamodb-integration-tests-bigdecimal", "dynamodb-integration-tests-string",
407 lowLevelClient.describeTable(req -> req.tableName(table)).get();
408 } catch (ExecutionException e) {
409 if (e.getCause() instanceof ResourceNotFoundException) {
410 // Table does not exist, this table does not need cleaning, continue to next table
415 lowLevelClient.deleteTable(req -> req.tableName(table)).get();
416 final WaiterResponse<DescribeTableResponse> waiterResponse;
417 waiterResponse = lowLevelClient.waiter().waitUntilTableNotExists(req -> req.tableName(table)).get();
418 Optional<Throwable> waiterException = waiterResponse.matched().exception()
419 .filter(e -> !(e instanceof ResourceNotFoundException));
420 assertTrue(waiterException.isEmpty(), waiterException::toString);
421 } catch (ExecutionException | InterruptedException e) {
422 fail("Error cleaning up test (deleting table)", e);