]> git.basschouten.com Git - openhab-addons.git/blob
f619813af3ecb61cbf49e9ac06af19b010a87721
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.concurrent.ExecutionException;
26 import java.util.stream.Stream;
27
28 import javax.measure.Unit;
29 import javax.measure.quantity.Dimensionless;
30 import javax.measure.quantity.Temperature;
31
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.junit.jupiter.api.AfterAll;
35 import org.junit.jupiter.api.BeforeAll;
36 import org.junit.jupiter.api.TestInfo;
37 import org.mockito.Mockito;
38 import org.openhab.core.common.registry.RegistryChangeListener;
39 import org.openhab.core.i18n.UnitProvider;
40 import org.openhab.core.internal.i18n.I18nProviderImpl;
41 import org.openhab.core.items.GenericItem;
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;
66
67 import com.amazonaws.services.dynamodbv2.local.main.ServerRunner;
68 import com.amazonaws.services.dynamodbv2.local.server.DynamoDBProxyServer;
69
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;
74
75 /**
76  *
77  * @author Sami Salonen - Initial contribution
78  *
79  */
80 @NonNullByDefault
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;
88     /*
89      * SI system has Celsius as temperature unit
90      */
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;
94
95     protected static UnitProvider UNIT_PROVIDER;
96     static {
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);
104     }
105
106     /**
107      * Whether tests are run in Continuous Integration environment, i.e. Jenkins or Travis CI
108      *
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
111      *
112      * @return
113      */
114     protected static boolean isRunningInCI() {
115         String jenkinsHome = System.getenv("JENKINS_HOME");
116         return "true".equals(System.getenv("CI")) || (jenkinsHome != null && !jenkinsHome.isBlank());
117     }
118
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();
123     }
124
125     private static int findFreeTCPPort() {
126         try (ServerSocket serverSocket = new ServerSocket(0)) {
127             int localPort = serverSocket.getLocalPort();
128             assertTrue(localPort > 0);
129             return localPort;
130         } catch (Exception e) {
131             fail("Unable to find free tcp port for embedded DynamoDB server");
132             return -1; // Make compiler happy
133         }
134     }
135
136     @Override
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);
142     }
143
144     @BeforeAll
145     protected static void populateItems() {
146         ITEMS.put("dimmer", new DimmerItem("dimmer"));
147         ITEMS.put("number", new NumberItem("number"));
148
149         NumberItem temperatureItem = new NumberItem("Number:Temperature", "numberTemperature");
150         ITEMS.put("numberTemperature", temperatureItem);
151         GroupItem groupTemperature = new GroupItem("groupNumberTemperature", temperatureItem);
152         ITEMS.put("groupNumberTemperature", groupTemperature);
153
154         NumberItem dimensionlessItem = new NumberItem("Number:Dimensionless", "numberDimensionless");
155         ITEMS.put("numberDimensionless", dimensionlessItem);
156         GroupItem groupDimensionless = new GroupItem("groupNumberDimensionless", dimensionlessItem);
157         ITEMS.put("groupNumberDimensionless", groupDimensionless);
158
159         GroupItem groupDummy = new GroupItem("dummyGroup", null);
160         ITEMS.put("groupDummy", groupDummy);
161
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"));
172
173         injectItemServices();
174     }
175
176     @BeforeAll
177     public static void initService(TestInfo testInfo) throws InterruptedException, IllegalArgumentException,
178             IllegalAccessException, NoSuchFieldException, SecurityException {
179         service = newService(isLegacyTest(testInfo), true, null, null, null);
180         clearData();
181     }
182
183     /**
184      * Create new persistence service. Either pointing to real DynamoDB (given credentials as java properties) or local
185      * in-memory server
186      *
187      * @param legacy whether to create config that implies legacy or new schema. Use null for MAYBE_LEGACY
188      * @param cleanLocal when creating local DB, whether to create new DB
189      * @param overrideLocalURI URI to use when using local DB
190      * @param table
191      * @param tablePrefix
192      * @return new persistence service
193      */
194     protected synchronized static DynamoDBPersistenceService newService(@Nullable Boolean legacy, boolean cleanLocal,
195             @Nullable URI overrideLocalURI, @Nullable String table, @Nullable String tablePrefix) {
196         final DynamoDBPersistenceService service;
197         Map<String, Object> config = getConfig(legacy, table, tablePrefix);
198         if (cleanLocal && overrideLocalURI != null) {
199             throw new IllegalArgumentException("cannot specify both cleanLocal=true and overrideLocalURI");
200         }
201         if (legacy == null && (table != null || tablePrefix != null)) {
202             throw new IllegalArgumentException("cannot specify both legacy=null and unambiguous table configuration");
203         }
204         URI localEndpointOverride = overrideLocalURI == null ? endpointOverride : overrideLocalURI;
205         if (overrideLocalURI == null && !credentialsSet() && (cleanLocal || endpointOverride == null)) {
206             // Local server not started yet, start it
207             // endpointOverride static field has the URI
208             LOGGER.info("Since credentials have not been defined, using embedded local AWS DynamoDB server");
209             System.setProperty("sqlite4java.library.path", "src/test/resources/native-libs");
210             int port = findFreeTCPPort();
211             String endpoint = String.format("http://127.0.0.1:%d", port);
212             try {
213                 localEndpointOverride = new URI(endpoint);
214                 DynamoDBProxyServer localEmbeddedServer = ServerRunner
215                         .createServerFromCommandLineArgs(new String[] { "-inMemory", "-port", String.valueOf(port) });
216                 localEmbeddedServer.start();
217                 embeddedServer = localEmbeddedServer;
218             } catch (Exception e) {
219                 fail("Error with embedded DynamoDB server", e);
220                 throw new IllegalStateException();
221             }
222         }
223
224         if (endpointOverride == null) {
225             endpointOverride = localEndpointOverride;
226         }
227
228         service = new DynamoDBPersistenceService(new ItemRegistry() {
229             @Override
230             public Collection<Item> getItems(String pattern) {
231                 throw new UnsupportedOperationException();
232             }
233
234             @Override
235             public Collection<Item> getItems() {
236                 throw new UnsupportedOperationException();
237             }
238
239             @Override
240             public Item getItemByPattern(String name) throws ItemNotFoundException, ItemNotUniqueException {
241                 throw new UnsupportedOperationException();
242             }
243
244             @Override
245             public Item getItem(String name) throws ItemNotFoundException {
246                 Item item = ITEMS.get(name);
247                 if (item == null) {
248                     throw new ItemNotFoundException(name);
249                 }
250                 injectItemServices(item);
251                 return item;
252             }
253
254             @Override
255             public void addRegistryChangeListener(RegistryChangeListener<Item> listener) {
256                 throw new UnsupportedOperationException();
257             }
258
259             @Override
260             public Collection<Item> getAll() {
261                 throw new UnsupportedOperationException();
262             }
263
264             @Override
265             public Stream<Item> stream() {
266                 throw new UnsupportedOperationException();
267             }
268
269             @Override
270             public @Nullable Item get(String key) {
271                 throw new UnsupportedOperationException();
272             }
273
274             @Override
275             public void removeRegistryChangeListener(RegistryChangeListener<Item> listener) {
276                 throw new UnsupportedOperationException();
277             }
278
279             @Override
280             public Item add(Item element) {
281                 throw new UnsupportedOperationException();
282             }
283
284             @Override
285             public @Nullable Item update(Item element) {
286                 throw new UnsupportedOperationException();
287             }
288
289             @Override
290             public @Nullable Item remove(String key) {
291                 throw new UnsupportedOperationException();
292             }
293
294             @Override
295             public Collection<Item> getItemsOfType(String type) {
296                 throw new UnsupportedOperationException();
297             }
298
299             @Override
300             public Collection<Item> getItemsByTag(String... tags) {
301                 throw new UnsupportedOperationException();
302             }
303
304             @Override
305             public Collection<Item> getItemsByTagAndType(String type, String... tags) {
306                 throw new UnsupportedOperationException();
307             }
308
309             @Override
310             public <T extends Item> Collection<T> getItemsByTag(Class<T> typeFilter, String... tags) {
311                 throw new UnsupportedOperationException();
312             }
313
314             @Override
315             public @Nullable Item remove(String itemName, boolean recursive) {
316                 throw new UnsupportedOperationException();
317             }
318
319             @Override
320             public void addRegistryHook(RegistryHook<Item> hook) {
321                 throw new UnsupportedOperationException();
322             }
323
324             @Override
325             public void removeRegistryHook(RegistryHook<Item> hook) {
326                 throw new UnsupportedOperationException();
327             }
328         }, localEndpointOverride);
329
330         service.activate(null, config);
331         return service;
332     }
333
334     protected static void injectItemServices() {
335         ITEMS.values().forEach(BaseIntegrationTest::injectItemServices);
336     }
337
338     protected static void injectItemServices(Item item) {
339         if (item instanceof GenericItem) {
340             GenericItem genericItem = (GenericItem) item;
341             genericItem.setUnitProvider(UNIT_PROVIDER);
342         }
343     }
344
345     private static Map<String, Object> getConfig(@Nullable Boolean legacy, @Nullable String table,
346             @Nullable String tablePrefix) {
347         Map<String, Object> config = new HashMap<>();
348         if (legacy != null) {
349             if (legacy.booleanValue()) {
350                 LOGGER.info("Legacy test");
351                 config.put("tablePrefix", tablePrefix == null ? TABLE_PREFIX : tablePrefix);
352             } else {
353                 LOGGER.info("Non-legacy test");
354                 config.put("table", table == null ? TABLE : table);
355                 config.put("expireDays", "1");
356             }
357         }
358
359         if (credentialsSet()) {
360             LOGGER.info("Since credentials have been defined, using real AWS DynamoDB");
361
362             String value = System.getProperty("DYNAMODBTEST_REGION");
363             config.put("region", value != null ? value : "");
364             value = System.getProperty("DYNAMODBTEST_ACCESS");
365             config.put("accessKey", value != null ? value : "");
366             value = System.getProperty("DYNAMODBTEST_SECRET");
367             config.put("secretKey", value != null ? value : "");
368
369             for (Entry<String, Object> entry : config.entrySet()) {
370                 if (((String) entry.getValue()).isEmpty()) {
371                     fail("Expecting " + entry.getKey()
372                             + " to have value for integration tests. Integration test will fail");
373                     throw new IllegalArgumentException();
374                 }
375             }
376         } else {
377             // Place some values to pass the configuration validation
378             config.put("region", "eu-west-1");
379             config.put("accessKey", "dummy-access-key");
380             config.put("secretKey", "dummy-secret-key");
381         }
382         return config;
383     }
384
385     protected static boolean isLegacyTest(TestInfo testInfo) {
386         try {
387             return testInfo.getTestClass().get().getDeclaredField("LEGACY_MODE").getBoolean(null);
388         } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) {
389             fail("Could not find static boolean LEGACY_MODE from the test class: " + e.getClass().getSimpleName() + " "
390                     + e.getMessage());
391             throw new IllegalStateException(); // Making compiler happy
392         }
393     }
394
395     protected boolean hasFakeServer() {
396         return embeddedServer != null;
397     }
398
399     @AfterAll
400     public static void tearDown() {
401         try {
402             if (embeddedServer != null) {
403                 embeddedServer.stop();
404             }
405         } catch (Exception e) {
406             fail("Error stopping embedded server", e);
407         }
408     }
409
410     protected static void clearData() {
411         DynamoDBPersistenceService localService = service;
412         assert localService != null;
413         DynamoDbAsyncClient lowLevelClient = localService.getLowLevelClient();
414         assertNotNull(lowLevelClient);
415         assert lowLevelClient != null;// To get rid of null exception
416         // Clear data
417         for (String table : new String[] { "dynamodb-integration-tests-bigdecimal", "dynamodb-integration-tests-string",
418                 TABLE }) {
419             try {
420                 try {
421                     lowLevelClient.describeTable(req -> req.tableName(table)).get();
422                 } catch (ExecutionException e) {
423                     if (e.getCause() instanceof ResourceNotFoundException) {
424                         // Table does not exist, this table does not need cleaning, continue to next table
425                         continue;
426                     }
427                 }
428
429                 lowLevelClient.deleteTable(req -> req.tableName(table)).get();
430                 final WaiterResponse<DescribeTableResponse> waiterResponse;
431                 try {
432                     waiterResponse = lowLevelClient.waiter().waitUntilTableNotExists(req -> req.tableName(table)).get();
433                 } catch (ExecutionException e) {
434                     // the waiting might fail with SdkClientException: An exception was thrown and did not match any
435                     // waiter acceptors
436                     // (the exception being CompletionException of ResourceNotFound)
437
438                     // We check if table has been removed, and continue if it has
439                     try {
440                         lowLevelClient.describeTable(req -> req.tableName(table)).get();
441                     } catch (ExecutionException e2) {
442                         if (e2.getCause() instanceof ResourceNotFoundException) {
443                             // Table does not exist, this table does not need cleaning, continue to next table
444                             continue;
445                         }
446                     }
447                     throw e;
448                 }
449                 assertTrue(waiterResponse.matched().exception().isEmpty());
450             } catch (ExecutionException | InterruptedException e) {
451                 fail("Error cleaning up test (deleting table)", e);
452             }
453         }
454     }
455 }