]> git.basschouten.com Git - openhab-addons.git/blob
b0f1fb390b63c87737041c609c57095354d2f866
[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.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 final 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<>();
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     private static boolean credentialsSet() {
107         String access = System.getProperty("DYNAMODBTEST_ACCESS");
108         String secret = System.getProperty("DYNAMODBTEST_SECRET");
109         return access != null && !access.isBlank() && secret != null && !secret.isBlank();
110     }
111
112     private static int findFreeTCPPort() {
113         try (ServerSocket serverSocket = new ServerSocket(0)) {
114             int localPort = serverSocket.getLocalPort();
115             assertTrue(localPort > 0);
116             return localPort;
117         } catch (Exception e) {
118             fail("Unable to find free tcp port for embedded DynamoDB server");
119             return -1; // Make compiler happy
120         }
121     }
122
123     @Override
124     protected void waitForAssert(Runnable runnable) {
125         // Use longer timeouts and slower polling with real dynamodb when credentials are set.
126         // Otherwise, test against a local server with lower timeouts.
127         waitForAssert(runnable, hasFakeServer() ? 30_000L : 120_000L, hasFakeServer() ? 500 : 1000L);
128     }
129
130     @BeforeAll
131     protected static void populateItems() {
132         ITEMS.put("dimmer", new DimmerItem("dimmer"));
133         ITEMS.put("number", new NumberItem("number"));
134
135         NumberItem temperatureItem = new NumberItem("Number:Temperature", "numberTemperature", UNIT_PROVIDER);
136         ITEMS.put("numberTemperature", temperatureItem);
137         GroupItem groupTemperature = new GroupItem("groupNumberTemperature", temperatureItem);
138         ITEMS.put("groupNumberTemperature", groupTemperature);
139
140         NumberItem dimensionlessItem = new NumberItem("Number:Dimensionless", "numberDimensionless", UNIT_PROVIDER);
141         ITEMS.put("numberDimensionless", dimensionlessItem);
142         GroupItem groupDimensionless = new GroupItem("groupNumberDimensionless", dimensionlessItem);
143         ITEMS.put("groupNumberDimensionless", groupDimensionless);
144
145         GroupItem groupDummy = new GroupItem("dummyGroup", null);
146         ITEMS.put("groupDummy", groupDummy);
147
148         ITEMS.put("string", new StringItem("string"));
149         ITEMS.put("switch", new SwitchItem("switch"));
150         ITEMS.put("contact", new ContactItem("contact"));
151         ITEMS.put("color", new ColorItem("color"));
152         ITEMS.put("rollershutter", new RollershutterItem("rollershutter"));
153         ITEMS.put("datetime", new DateTimeItem("datetime"));
154         ITEMS.put("call", new CallItem("call"));
155         ITEMS.put("location", new LocationItem("location"));
156         ITEMS.put("player_playpause", new PlayerItem("player_playpause"));
157         ITEMS.put("player_rewindfastforward", new PlayerItem("player_rewindfastforward"));
158     }
159
160     @BeforeAll
161     public static void initService(TestInfo testInfo) throws InterruptedException, IllegalArgumentException,
162             IllegalAccessException, NoSuchFieldException, SecurityException {
163         service = newService(isLegacyTest(testInfo), true, null, null, null);
164         clearData();
165     }
166
167     /**
168      * Create new persistence service. Either pointing to real DynamoDB (given credentials as java properties) or local
169      * in-memory server
170      *
171      * @param legacy whether to create config that implies legacy or new schema. Use null for MAYBE_LEGACY
172      * @param cleanLocal when creating local DB, whether to create new DB
173      * @param overrideLocalURI URI to use when using local DB
174      * @param table
175      * @param tablePrefix
176      * @return new persistence service
177      */
178     protected static synchronized DynamoDBPersistenceService newService(@Nullable Boolean legacy, boolean cleanLocal,
179             @Nullable URI overrideLocalURI, @Nullable String table, @Nullable String tablePrefix) {
180         final DynamoDBPersistenceService service;
181         Map<String, Object> config = getConfig(legacy, table, tablePrefix);
182         if (cleanLocal && overrideLocalURI != null) {
183             throw new IllegalArgumentException("cannot specify both cleanLocal=true and overrideLocalURI");
184         }
185         if (legacy == null && (table != null || tablePrefix != null)) {
186             throw new IllegalArgumentException("cannot specify both legacy=null and unambiguous table configuration");
187         }
188         URI localEndpointOverride = overrideLocalURI == null ? endpointOverride : overrideLocalURI;
189         if (overrideLocalURI == null && !credentialsSet() && (cleanLocal || endpointOverride == null)) {
190             // Local server not started yet, start it
191             // endpointOverride static field has the URI
192             LOGGER.info("Since credentials have not been defined, using embedded local AWS DynamoDB server");
193             System.setProperty("sqlite4java.library.path", "src/test/resources/native-libs");
194             int port = findFreeTCPPort();
195             String endpoint = String.format("http://127.0.0.1:%d", port);
196             try {
197                 localEndpointOverride = new URI(endpoint);
198                 DynamoDBProxyServer localEmbeddedServer = ServerRunner
199                         .createServerFromCommandLineArgs(new String[] { "-inMemory", "-port", String.valueOf(port) });
200                 localEmbeddedServer.start();
201                 embeddedServer = localEmbeddedServer;
202             } catch (Exception e) {
203                 fail("Error with embedded DynamoDB server", e);
204                 throw new IllegalStateException();
205             }
206         }
207
208         if (endpointOverride == null) {
209             endpointOverride = localEndpointOverride;
210         }
211
212         service = new DynamoDBPersistenceService(new ItemRegistry() {
213             @Override
214             public Collection<Item> getItems(String pattern) {
215                 throw new UnsupportedOperationException();
216             }
217
218             @Override
219             public Collection<Item> getItems() {
220                 throw new UnsupportedOperationException();
221             }
222
223             @Override
224             public Item getItemByPattern(String name) throws ItemNotFoundException, ItemNotUniqueException {
225                 throw new UnsupportedOperationException();
226             }
227
228             @Override
229             public Item getItem(String name) throws ItemNotFoundException {
230                 Item item = ITEMS.get(name);
231                 if (item == null) {
232                     throw new ItemNotFoundException(name);
233                 }
234                 return item;
235             }
236
237             @Override
238             public void addRegistryChangeListener(RegistryChangeListener<Item> listener) {
239                 throw new UnsupportedOperationException();
240             }
241
242             @Override
243             public Collection<Item> getAll() {
244                 throw new UnsupportedOperationException();
245             }
246
247             @Override
248             public Stream<Item> stream() {
249                 throw new UnsupportedOperationException();
250             }
251
252             @Override
253             public @Nullable Item get(String key) {
254                 throw new UnsupportedOperationException();
255             }
256
257             @Override
258             public void removeRegistryChangeListener(RegistryChangeListener<Item> listener) {
259                 throw new UnsupportedOperationException();
260             }
261
262             @Override
263             public Item add(Item element) {
264                 throw new UnsupportedOperationException();
265             }
266
267             @Override
268             public @Nullable Item update(Item element) {
269                 throw new UnsupportedOperationException();
270             }
271
272             @Override
273             public @Nullable Item remove(String key) {
274                 throw new UnsupportedOperationException();
275             }
276
277             @Override
278             public Collection<Item> getItemsOfType(String type) {
279                 throw new UnsupportedOperationException();
280             }
281
282             @Override
283             public Collection<Item> getItemsByTag(String... tags) {
284                 throw new UnsupportedOperationException();
285             }
286
287             @Override
288             public Collection<Item> getItemsByTagAndType(String type, String... tags) {
289                 throw new UnsupportedOperationException();
290             }
291
292             @Override
293             public <T extends Item> Collection<T> getItemsByTag(Class<T> typeFilter, String... tags) {
294                 throw new UnsupportedOperationException();
295             }
296
297             @Override
298             public @Nullable Item remove(String itemName, boolean recursive) {
299                 throw new UnsupportedOperationException();
300             }
301
302             @Override
303             public void addRegistryHook(RegistryHook<Item> hook) {
304                 throw new UnsupportedOperationException();
305             }
306
307             @Override
308             public void removeRegistryHook(RegistryHook<Item> hook) {
309                 throw new UnsupportedOperationException();
310             }
311         }, UNIT_PROVIDER, localEndpointOverride);
312
313         service.activate(null, config);
314         return service;
315     }
316
317     private static Map<String, Object> getConfig(@Nullable Boolean legacy, @Nullable String table,
318             @Nullable String tablePrefix) {
319         Map<String, Object> config = new HashMap<>();
320         if (legacy != null) {
321             if (legacy.booleanValue()) {
322                 LOGGER.info("Legacy test");
323                 config.put("tablePrefix", tablePrefix == null ? TABLE_PREFIX : tablePrefix);
324             } else {
325                 LOGGER.info("Non-legacy test");
326                 config.put("table", table == null ? TABLE : table);
327                 config.put("expireDays", "1");
328             }
329         }
330
331         if (credentialsSet()) {
332             LOGGER.info("Since credentials have been defined, using real AWS DynamoDB");
333
334             String value = System.getProperty("DYNAMODBTEST_REGION");
335             config.put("region", value != null ? value : "");
336             value = System.getProperty("DYNAMODBTEST_ACCESS");
337             config.put("accessKey", value != null ? value : "");
338             value = System.getProperty("DYNAMODBTEST_SECRET");
339             config.put("secretKey", value != null ? value : "");
340
341             for (Entry<String, Object> entry : config.entrySet()) {
342                 if (((String) entry.getValue()).isEmpty()) {
343                     fail("Expecting " + entry.getKey()
344                             + " to have value for integration tests. Integration test will fail");
345                     throw new IllegalArgumentException();
346                 }
347             }
348         } else {
349             // Place some values to pass the configuration validation
350             config.put("region", "eu-west-1");
351             config.put("accessKey", "dummy-access-key");
352             config.put("secretKey", "dummy-secret-key");
353         }
354         return config;
355     }
356
357     protected static boolean isLegacyTest(TestInfo testInfo) {
358         try {
359             return testInfo.getTestClass().get().getDeclaredField("LEGACY_MODE").getBoolean(null);
360         } catch (IllegalArgumentException | IllegalAccessException | NoSuchFieldException | SecurityException e) {
361             fail("Could not find static boolean LEGACY_MODE from the test class: " + e.getClass().getSimpleName() + " "
362                     + e.getMessage());
363             throw new IllegalStateException(); // Making compiler happy
364         }
365     }
366
367     protected boolean hasFakeServer() {
368         return embeddedServer != null;
369     }
370
371     @AfterAll
372     public static void tearDown() {
373         try {
374             if (embeddedServer != null) {
375                 embeddedServer.stop();
376             }
377         } catch (Exception e) {
378             fail("Error stopping embedded server", e);
379         }
380     }
381
382     protected static void clearData() {
383         DynamoDBPersistenceService localService = service;
384         assert localService != null;
385         DynamoDbAsyncClient lowLevelClient = localService.getLowLevelClient();
386         assertNotNull(lowLevelClient);
387         assert lowLevelClient != null;// To get rid of null exception
388         // Clear data
389         for (String table : new String[] { "dynamodb-integration-tests-bigdecimal", "dynamodb-integration-tests-string",
390                 TABLE }) {
391             try {
392                 try {
393                     lowLevelClient.describeTable(req -> req.tableName(table)).get();
394                 } catch (ExecutionException e) {
395                     if (e.getCause() instanceof ResourceNotFoundException) {
396                         // Table does not exist, this table does not need cleaning, continue to next table
397                         continue;
398                     }
399                 }
400
401                 lowLevelClient.deleteTable(req -> req.tableName(table)).get();
402                 final WaiterResponse<DescribeTableResponse> waiterResponse;
403                 waiterResponse = lowLevelClient.waiter().waitUntilTableNotExists(req -> req.tableName(table)).get();
404                 Optional<Throwable> waiterException = waiterResponse.matched().exception()
405                         .filter(e -> !(e instanceof ResourceNotFoundException));
406                 assertTrue(waiterException.isEmpty(), waiterException::toString);
407             } catch (ExecutionException | InterruptedException e) {
408                 fail("Error cleaning up test (deleting table)", e);
409             }
410         }
411     }
412 }