]> git.basschouten.com Git - openhab-addons.git/blob
f3e5d38bdec68dd99467c74aca457138ebd2321d
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.persistence.dynamodb.internal;
14
15 import static org.junit.jupiter.api.Assertions.*;
16 import static org.mockito.Mockito.when;
17
18 import java.net.ServerSocket;
19 import java.net.URI;
20 import java.util.Collection;
21 import java.util.HashMap;
22 import java.util.Hashtable;
23 import java.util.Map;
24 import java.util.Map.Entry;
25 import java.util.Optional;
26 import java.util.concurrent.ExecutionException;
27 import java.util.stream.Stream;
28
29 import javax.measure.Unit;
30 import javax.measure.quantity.Dimensionless;
31 import javax.measure.quantity.Temperature;
32
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.GenericItem;
43 import org.openhab.core.items.GroupItem;
44 import org.openhab.core.items.Item;
45 import org.openhab.core.items.ItemNotFoundException;
46 import org.openhab.core.items.ItemNotUniqueException;
47 import org.openhab.core.items.ItemRegistry;
48 import org.openhab.core.items.RegistryHook;
49 import org.openhab.core.library.items.CallItem;
50 import org.openhab.core.library.items.ColorItem;
51 import org.openhab.core.library.items.ContactItem;
52 import org.openhab.core.library.items.DateTimeItem;
53 import org.openhab.core.library.items.DimmerItem;
54 import org.openhab.core.library.items.LocationItem;
55 import org.openhab.core.library.items.NumberItem;
56 import org.openhab.core.library.items.PlayerItem;
57 import org.openhab.core.library.items.RollershutterItem;
58 import org.openhab.core.library.items.StringItem;
59 import org.openhab.core.library.items.SwitchItem;
60 import org.openhab.core.library.unit.SIUnits;
61 import org.openhab.core.library.unit.Units;
62 import org.openhab.core.test.java.JavaTest;
63 import org.osgi.framework.BundleContext;
64 import org.osgi.service.component.ComponentContext;
65 import org.slf4j.Logger;
66 import org.slf4j.LoggerFactory;
67
68 import com.amazonaws.services.dynamodbv2.local.main.ServerRunner;
69 import com.amazonaws.services.dynamodbv2.local.server.DynamoDBProxyServer;
70
71 import software.amazon.awssdk.core.waiters.WaiterResponse;
72 import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
73 import software.amazon.awssdk.services.dynamodb.model.DescribeTableResponse;
74 import software.amazon.awssdk.services.dynamodb.model.ResourceNotFoundException;
75
76 /**
77  *
78  * @author Sami Salonen - Initial contribution
79  *
80  */
81 @NonNullByDefault
82 public class BaseIntegrationTest extends JavaTest {
83     protected static final String TABLE = "dynamodb-integration-tests";
84     protected static final String TABLE_PREFIX = "dynamodb-integration-tests-";
85     protected static final Logger LOGGER = LoggerFactory.getLogger(DynamoDBPersistenceService.class);
86     protected static @Nullable DynamoDBPersistenceService service;
87     protected static final Map<String, Item> ITEMS = new HashMap<>();
88     protected static @Nullable DynamoDBProxyServer embeddedServer;
89     /*
90      * SI system has Celsius as temperature unit
91      */
92     protected static final Unit<Temperature> TEMP_ITEM_UNIT = SIUnits.CELSIUS;
93     protected static final Unit<Dimensionless> DIMENSIONLESS_ITEM_UNIT = Units.ONE;
94     private static @Nullable URI endpointOverride;
95
96     protected static final UnitProvider UNIT_PROVIDER;
97     static {
98         ComponentContext context = Mockito.mock(ComponentContext.class);
99         BundleContext bundleContext = Mockito.mock(BundleContext.class);
100         Hashtable<String, Object> properties = new Hashtable<String, Object>();
101         properties.put("measurementSystem", SIUnits.MEASUREMENT_SYSTEM_NAME);
102         when(context.getProperties()).thenReturn(properties);
103         when(context.getBundleContext()).thenReturn(bundleContext);
104         UNIT_PROVIDER = new I18nProviderImpl(context);
105     }
106
107     /**
108      * Whether tests are run in Continuous Integration environment, i.e. Jenkins or Travis CI
109      *
110      * Travis CI is detected using CI environment variable, see https://docs.travis-ci.com/user/environment-variables/
111      * Jenkins CI is detected using JENKINS_HOME environment variable
112      *
113      * @return
114      */
115     protected static boolean isRunningInCI() {
116         String jenkinsHome = System.getenv("JENKINS_HOME");
117         return "true".equals(System.getenv("CI")) || (jenkinsHome != null && !jenkinsHome.isBlank());
118     }
119
120     private static boolean credentialsSet() {
121         String access = System.getProperty("DYNAMODBTEST_ACCESS");
122         String secret = System.getProperty("DYNAMODBTEST_SECRET");
123         return access != null && !access.isBlank() && secret != null && !secret.isBlank();
124     }
125
126     private static int findFreeTCPPort() {
127         try (ServerSocket serverSocket = new ServerSocket(0)) {
128             int localPort = serverSocket.getLocalPort();
129             assertTrue(localPort > 0);
130             return localPort;
131         } catch (Exception e) {
132             fail("Unable to find free tcp port for embedded DynamoDB server");
133             return -1; // Make compiler happy
134         }
135     }
136
137     @Override
138     protected void waitForAssert(Runnable runnable) {
139         // Longer timeouts and slower polling with real dynamodb
140         // Non-CI tests against local server are with lower timeout.
141         waitForAssert(runnable, hasFakeServer() ? isRunningInCI() ? 30_000L : 10_000L : 120_000L,
142                 hasFakeServer() ? 500 : 1000L);
143     }
144
145     @BeforeAll
146     protected static void populateItems() {
147         ITEMS.put("dimmer", new DimmerItem("dimmer"));
148         ITEMS.put("number", new NumberItem("number"));
149
150         NumberItem temperatureItem = new NumberItem("Number:Temperature", "numberTemperature");
151         ITEMS.put("numberTemperature", temperatureItem);
152         GroupItem groupTemperature = new GroupItem("groupNumberTemperature", temperatureItem);
153         ITEMS.put("groupNumberTemperature", groupTemperature);
154
155         NumberItem dimensionlessItem = new NumberItem("Number:Dimensionless", "numberDimensionless");
156         ITEMS.put("numberDimensionless", dimensionlessItem);
157         GroupItem groupDimensionless = new GroupItem("groupNumberDimensionless", dimensionlessItem);
158         ITEMS.put("groupNumberDimensionless", groupDimensionless);
159
160         GroupItem groupDummy = new GroupItem("dummyGroup", null);
161         ITEMS.put("groupDummy", groupDummy);
162
163         ITEMS.put("string", new StringItem("string"));
164         ITEMS.put("switch", new SwitchItem("switch"));
165         ITEMS.put("contact", new ContactItem("contact"));
166         ITEMS.put("color", new ColorItem("color"));
167         ITEMS.put("rollershutter", new RollershutterItem("rollershutter"));
168         ITEMS.put("datetime", new DateTimeItem("datetime"));
169         ITEMS.put("call", new CallItem("call"));
170         ITEMS.put("location", new LocationItem("location"));
171         ITEMS.put("player_playpause", new PlayerItem("player_playpause"));
172         ITEMS.put("player_rewindfastforward", new PlayerItem("player_rewindfastforward"));
173
174         injectItemServices();
175     }
176
177     @BeforeAll
178     public static void initService(TestInfo testInfo) throws InterruptedException, IllegalArgumentException,
179             IllegalAccessException, NoSuchFieldException, SecurityException {
180         service = newService(isLegacyTest(testInfo), true, null, null, null);
181         clearData();
182     }
183
184     /**
185      * Create new persistence service. Either pointing to real DynamoDB (given credentials as java properties) or local
186      * in-memory server
187      *
188      * @param legacy whether to create config that implies legacy or new schema. Use null for MAYBE_LEGACY
189      * @param cleanLocal when creating local DB, whether to create new DB
190      * @param overrideLocalURI URI to use when using local DB
191      * @param table
192      * @param tablePrefix
193      * @return new persistence service
194      */
195     protected static synchronized DynamoDBPersistenceService newService(@Nullable Boolean legacy, boolean cleanLocal,
196             @Nullable URI overrideLocalURI, @Nullable String table, @Nullable String tablePrefix) {
197         final DynamoDBPersistenceService service;
198         Map<String, Object> config = getConfig(legacy, table, tablePrefix);
199         if (cleanLocal && overrideLocalURI != null) {
200             throw new IllegalArgumentException("cannot specify both cleanLocal=true and overrideLocalURI");
201         }
202         if (legacy == null && (table != null || tablePrefix != null)) {
203             throw new IllegalArgumentException("cannot specify both legacy=null and unambiguous table configuration");
204         }
205         URI localEndpointOverride = overrideLocalURI == null ? endpointOverride : overrideLocalURI;
206         if (overrideLocalURI == null && !credentialsSet() && (cleanLocal || endpointOverride == null)) {
207             // Local server not started yet, start it
208             // endpointOverride static field has the URI
209             LOGGER.info("Since credentials have not been defined, using embedded local AWS DynamoDB server");
210             System.setProperty("sqlite4java.library.path", "src/test/resources/native-libs");
211             int port = findFreeTCPPort();
212             String endpoint = String.format("http://127.0.0.1:%d", port);
213             try {
214                 localEndpointOverride = new URI(endpoint);
215                 DynamoDBProxyServer localEmbeddedServer = ServerRunner
216                         .createServerFromCommandLineArgs(new String[] { "-inMemory", "-port", String.valueOf(port) });
217                 localEmbeddedServer.start();
218                 embeddedServer = localEmbeddedServer;
219             } catch (Exception e) {
220                 fail("Error with embedded DynamoDB server", e);
221                 throw new IllegalStateException();
222             }
223         }
224
225         if (endpointOverride == null) {
226             endpointOverride = localEndpointOverride;
227         }
228
229         service = new DynamoDBPersistenceService(new ItemRegistry() {
230             @Override
231             public Collection<Item> getItems(String pattern) {
232                 throw new UnsupportedOperationException();
233             }
234
235             @Override
236             public Collection<Item> getItems() {
237                 throw new UnsupportedOperationException();
238             }
239
240             @Override
241             public Item getItemByPattern(String name) throws ItemNotFoundException, ItemNotUniqueException {
242                 throw new UnsupportedOperationException();
243             }
244
245             @Override
246             public Item getItem(String name) throws ItemNotFoundException {
247                 Item item = ITEMS.get(name);
248                 if (item == null) {
249                     throw new ItemNotFoundException(name);
250                 }
251                 injectItemServices(item);
252                 return item;
253             }
254
255             @Override
256             public void addRegistryChangeListener(RegistryChangeListener<Item> listener) {
257                 throw new UnsupportedOperationException();
258             }
259
260             @Override
261             public Collection<Item> getAll() {
262                 throw new UnsupportedOperationException();
263             }
264
265             @Override
266             public Stream<Item> stream() {
267                 throw new UnsupportedOperationException();
268             }
269
270             @Override
271             public @Nullable Item get(String key) {
272                 throw new UnsupportedOperationException();
273             }
274
275             @Override
276             public void removeRegistryChangeListener(RegistryChangeListener<Item> listener) {
277                 throw new UnsupportedOperationException();
278             }
279
280             @Override
281             public Item add(Item element) {
282                 throw new UnsupportedOperationException();
283             }
284
285             @Override
286             public @Nullable Item update(Item element) {
287                 throw new UnsupportedOperationException();
288             }
289
290             @Override
291             public @Nullable Item remove(String key) {
292                 throw new UnsupportedOperationException();
293             }
294
295             @Override
296             public Collection<Item> getItemsOfType(String type) {
297                 throw new UnsupportedOperationException();
298             }
299
300             @Override
301             public Collection<Item> getItemsByTag(String... tags) {
302                 throw new UnsupportedOperationException();
303             }
304
305             @Override
306             public Collection<Item> getItemsByTagAndType(String type, String... tags) {
307                 throw new UnsupportedOperationException();
308             }
309
310             @Override
311             public <T extends Item> Collection<T> getItemsByTag(Class<T> typeFilter, String... tags) {
312                 throw new UnsupportedOperationException();
313             }
314
315             @Override
316             public @Nullable Item remove(String itemName, boolean recursive) {
317                 throw new UnsupportedOperationException();
318             }
319
320             @Override
321             public void addRegistryHook(RegistryHook<Item> hook) {
322                 throw new UnsupportedOperationException();
323             }
324
325             @Override
326             public void removeRegistryHook(RegistryHook<Item> hook) {
327                 throw new UnsupportedOperationException();
328             }
329         }, localEndpointOverride);
330
331         service.activate(null, config);
332         return service;
333     }
334
335     protected static void injectItemServices() {
336         ITEMS.values().forEach(BaseIntegrationTest::injectItemServices);
337     }
338
339     protected static void injectItemServices(Item item) {
340         if (item instanceof GenericItem) {
341             GenericItem genericItem = (GenericItem) item;
342             genericItem.setUnitProvider(UNIT_PROVIDER);
343         }
344     }
345
346     private static Map<String, Object> getConfig(@Nullable Boolean legacy, @Nullable String table,
347             @Nullable String tablePrefix) {
348         Map<String, Object> config = new HashMap<>();
349         if (legacy != null) {
350             if (legacy.booleanValue()) {
351                 LOGGER.info("Legacy test");
352                 config.put("tablePrefix", tablePrefix == null ? TABLE_PREFIX : tablePrefix);
353             } else {
354                 LOGGER.info("Non-legacy test");
355                 config.put("table", table == null ? TABLE : table);
356                 config.put("expireDays", "1");
357             }
358         }
359
360         if (credentialsSet()) {
361             LOGGER.info("Since credentials have been defined, using real AWS DynamoDB");
362
363             String value = System.getProperty("DYNAMODBTEST_REGION");
364             config.put("region", value != null ? value : "");
365             value = System.getProperty("DYNAMODBTEST_ACCESS");
366             config.put("accessKey", value != null ? value : "");
367             value = System.getProperty("DYNAMODBTEST_SECRET");
368             config.put("secretKey", value != null ? value : "");
369
370             for (Entry<String, Object> entry : config.entrySet()) {
371                 if (((String) entry.getValue()).isEmpty()) {
372                     fail("Expecting " + entry.getKey()
373                             + " to have value for integration tests. Integration test will fail");
374                     throw new IllegalArgumentException();
375                 }
376             }
377         } else {
378             // Place some values to pass the configuration validation
379             config.put("region", "eu-west-1");
380             config.put("accessKey", "dummy-access-key");
381             config.put("secretKey", "dummy-secret-key");
382         }
383         return config;
384     }
385
386     protected static boolean isLegacyTest(TestInfo testInfo) {
387         try {
388             return testInfo.getTestClass().get().getDeclaredField("LEGACY_MODE").getBoolean(null);
389         } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) {
390             fail("Could not find static boolean LEGACY_MODE from the test class: " + e.getClass().getSimpleName() + " "
391                     + e.getMessage());
392             throw new IllegalStateException(); // Making compiler happy
393         }
394     }
395
396     protected boolean hasFakeServer() {
397         return embeddedServer != null;
398     }
399
400     @AfterAll
401     public static void tearDown() {
402         try {
403             if (embeddedServer != null) {
404                 embeddedServer.stop();
405             }
406         } catch (Exception e) {
407             fail("Error stopping embedded server", e);
408         }
409     }
410
411     protected static void clearData() {
412         DynamoDBPersistenceService localService = service;
413         assert localService != null;
414         DynamoDbAsyncClient lowLevelClient = localService.getLowLevelClient();
415         assertNotNull(lowLevelClient);
416         assert lowLevelClient != null;// To get rid of null exception
417         // Clear data
418         for (String table : new String[] { "dynamodb-integration-tests-bigdecimal", "dynamodb-integration-tests-string",
419                 TABLE }) {
420             try {
421                 try {
422                     lowLevelClient.describeTable(req -> req.tableName(table)).get();
423                 } catch (ExecutionException e) {
424                     if (e.getCause() instanceof ResourceNotFoundException) {
425                         // Table does not exist, this table does not need cleaning, continue to next table
426                         continue;
427                     }
428                 }
429
430                 lowLevelClient.deleteTable(req -> req.tableName(table)).get();
431                 final WaiterResponse<DescribeTableResponse> waiterResponse;
432                 waiterResponse = lowLevelClient.waiter().waitUntilTableNotExists(req -> req.tableName(table)).get();
433                 Optional<Throwable> waiterException = waiterResponse.matched().exception()
434                         .filter(e -> !(e instanceof ResourceNotFoundException));
435                 assertTrue(waiterException.isEmpty(), waiterException::toString);
436             } catch (ExecutionException | InterruptedException e) {
437                 fail("Error cleaning up test (deleting table)", e);
438             }
439         }
440     }
441 }