]> git.basschouten.com Git - openhab-addons.git/blob
98578166ec7ed1b0f41df5eef139f1559dff116e
[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.binding.nest.internal.wwn.handler;
14
15 import static org.hamcrest.CoreMatchers.*;
16 import static org.hamcrest.MatcherAssert.assertThat;
17 import static org.hamcrest.core.Is.is;
18 import static org.hamcrest.core.IsNot.not;
19 import static org.mockito.Mockito.*;
20 import static org.openhab.binding.nest.internal.wwn.rest.WWNStreamingRestClient.PUT;
21
22 import java.io.IOException;
23 import java.time.Instant;
24 import java.time.format.DateTimeParseException;
25 import java.util.ArrayList;
26 import java.util.HashMap;
27 import java.util.List;
28 import java.util.Locale;
29 import java.util.Map;
30 import java.util.TimeZone;
31 import java.util.function.Function;
32
33 import javax.ws.rs.client.ClientBuilder;
34
35 import org.eclipse.jdt.annotation.NonNullByDefault;
36 import org.eclipse.jetty.servlet.ServletHolder;
37 import org.junit.jupiter.api.AfterEach;
38 import org.junit.jupiter.api.BeforeAll;
39 import org.junit.jupiter.api.BeforeEach;
40 import org.mockito.ArgumentMatchers;
41 import org.openhab.binding.nest.internal.wwn.config.WWNAccountConfiguration;
42 import org.openhab.binding.nest.internal.wwn.test.WWNTestAccountHandler;
43 import org.openhab.binding.nest.internal.wwn.test.WWNTestApiServlet;
44 import org.openhab.binding.nest.internal.wwn.test.WWNTestHandlerFactory;
45 import org.openhab.binding.nest.internal.wwn.test.WWNTestServer;
46 import org.openhab.core.config.core.Configuration;
47 import org.openhab.core.events.EventPublisher;
48 import org.openhab.core.items.Item;
49 import org.openhab.core.items.ItemFactory;
50 import org.openhab.core.items.ItemNotFoundException;
51 import org.openhab.core.items.ItemRegistry;
52 import org.openhab.core.items.events.ItemEventFactory;
53 import org.openhab.core.library.types.DateTimeType;
54 import org.openhab.core.test.TestPortUtil;
55 import org.openhab.core.test.java.JavaOSGiTest;
56 import org.openhab.core.test.storage.VolatileStorageService;
57 import org.openhab.core.thing.Bridge;
58 import org.openhab.core.thing.Channel;
59 import org.openhab.core.thing.ChannelUID;
60 import org.openhab.core.thing.ManagedThingProvider;
61 import org.openhab.core.thing.Thing;
62 import org.openhab.core.thing.ThingProvider;
63 import org.openhab.core.thing.ThingTypeUID;
64 import org.openhab.core.thing.ThingUID;
65 import org.openhab.core.thing.binding.ThingHandlerFactory;
66 import org.openhab.core.thing.binding.ThingTypeProvider;
67 import org.openhab.core.thing.binding.builder.BridgeBuilder;
68 import org.openhab.core.thing.binding.builder.ChannelBuilder;
69 import org.openhab.core.thing.link.ItemChannelLink;
70 import org.openhab.core.thing.link.ManagedItemChannelLinkProvider;
71 import org.openhab.core.thing.type.ChannelDefinition;
72 import org.openhab.core.thing.type.ChannelGroupDefinition;
73 import org.openhab.core.thing.type.ChannelGroupType;
74 import org.openhab.core.thing.type.ChannelGroupTypeRegistry;
75 import org.openhab.core.thing.type.ChannelType;
76 import org.openhab.core.thing.type.ChannelTypeRegistry;
77 import org.openhab.core.thing.type.ThingType;
78 import org.openhab.core.thing.type.ThingTypeRegistry;
79 import org.openhab.core.types.Command;
80 import org.openhab.core.types.RefreshType;
81 import org.openhab.core.types.State;
82 import org.openhab.core.types.UnDefType;
83 import org.osgi.service.component.ComponentContext;
84 import org.osgi.service.jaxrs.client.SseEventSourceFactory;
85 import org.slf4j.Logger;
86 import org.slf4j.LoggerFactory;
87
88 /**
89  * {@link WWNThingHandlerOSGiTest} is an abstract base class for Nest OSGi based tests.
90  *
91  * @author Wouter Born - Initial contribution
92  */
93 public abstract class WWNThingHandlerOSGiTest extends JavaOSGiTest {
94
95     private static final String SERVER_HOST = "127.0.0.1";
96     private static final int SERVER_PORT = TestPortUtil.findFreePort();
97     private static final int SERVER_TIMEOUT = -1;
98     private static final String REDIRECT_URL = "http://" + SERVER_HOST + ":" + SERVER_PORT;
99
100     private final Logger logger = LoggerFactory.getLogger(WWNThingHandlerOSGiTest.class);
101
102     private static WWNTestServer server;
103     private static WWNTestApiServlet servlet = new WWNTestApiServlet();
104
105     private ChannelTypeRegistry channelTypeRegistry;
106     private ChannelGroupTypeRegistry channelGroupTypeRegistry;
107     private ItemFactory itemFactory;
108     private ItemRegistry itemRegistry;
109     private EventPublisher eventPublisher;
110     private ManagedThingProvider managedThingProvider;
111     private ThingTypeRegistry thingTypeRegistry;
112     private ManagedItemChannelLinkProvider managedItemChannelLinkProvider;
113     private VolatileStorageService volatileStorageService = new VolatileStorageService();
114
115     protected Bridge bridge;
116     protected WWNTestAccountHandler bridgeHandler;
117     protected Thing thing;
118     protected WWNBaseHandler<?> thingHandler;
119     private Class<? extends WWNBaseHandler<?>> thingClass;
120
121     private WWNTestHandlerFactory nestTestHandlerFactory;
122     private @NonNullByDefault({}) ClientBuilder clientBuilder;
123     private @NonNullByDefault({}) SseEventSourceFactory eventSourceFactory;
124
125     public WWNThingHandlerOSGiTest(Class<? extends WWNBaseHandler<?>> thingClass) {
126         this.thingClass = thingClass;
127     }
128
129     @BeforeAll
130     public static void setUpClass() throws Exception {
131         ServletHolder holder = new ServletHolder(servlet);
132         server = new WWNTestServer(SERVER_HOST, SERVER_PORT, SERVER_TIMEOUT, holder);
133         server.startServer();
134     }
135
136     @BeforeEach
137     public void setUp() throws ItemNotFoundException {
138         registerService(volatileStorageService);
139
140         managedThingProvider = getService(ThingProvider.class, ManagedThingProvider.class);
141         assertThat("Could not get ManagedThingProvider", managedThingProvider, is(notNullValue()));
142
143         thingTypeRegistry = getService(ThingTypeRegistry.class);
144         assertThat("Could not get ThingTypeRegistry", thingTypeRegistry, is(notNullValue()));
145
146         channelTypeRegistry = getService(ChannelTypeRegistry.class);
147         assertThat("Could not get ChannelTypeRegistry", channelTypeRegistry, is(notNullValue()));
148
149         channelGroupTypeRegistry = getService(ChannelGroupTypeRegistry.class);
150         assertThat("Could not get ChannelGroupTypeRegistry", channelGroupTypeRegistry, is(notNullValue()));
151
152         eventPublisher = getService(EventPublisher.class);
153         assertThat("Could not get EventPublisher", eventPublisher, is(notNullValue()));
154
155         itemFactory = getService(ItemFactory.class);
156         assertThat("Could not get ItemFactory", itemFactory, is(notNullValue()));
157
158         itemRegistry = getService(ItemRegistry.class);
159         assertThat("Could not get ItemRegistry", itemRegistry, is(notNullValue()));
160
161         managedItemChannelLinkProvider = getService(ManagedItemChannelLinkProvider.class);
162         assertThat("Could not get ManagedItemChannelLinkProvider", managedItemChannelLinkProvider, is(notNullValue()));
163
164         clientBuilder = getService(ClientBuilder.class);
165         assertThat("Could not get ClientBuilder", clientBuilder, is(notNullValue()));
166
167         eventSourceFactory = getService(SseEventSourceFactory.class);
168         assertThat("Could not get SseEventSourceFactory", eventSourceFactory, is(notNullValue()));
169
170         ComponentContext componentContext = mock(ComponentContext.class);
171         when(componentContext.getBundleContext()).thenReturn(bundleContext);
172
173         nestTestHandlerFactory = new WWNTestHandlerFactory(clientBuilder, eventSourceFactory);
174         nestTestHandlerFactory.activate(componentContext,
175                 Map.of(WWNTestHandlerFactory.REDIRECT_URL_CONFIG_PROPERTY, REDIRECT_URL));
176         registerService(nestTestHandlerFactory);
177
178         ThingTypeProvider thingTypeProvider = mock(ThingTypeProvider.class);
179         when(thingTypeProvider.getThingType(ArgumentMatchers.any(ThingTypeUID.class), nullable(Locale.class)))
180                 .thenReturn(mock(ThingType.class));
181         registerService(thingTypeProvider);
182
183         nestTestHandlerFactory = getService(ThingHandlerFactory.class, WWNTestHandlerFactory.class);
184         assertThat("Could not get NestTestHandlerFactory", nestTestHandlerFactory, is(notNullValue()));
185
186         bridge = buildBridge();
187         thing = buildThing(bridge);
188
189         bridgeHandler = addThing(bridge, WWNTestAccountHandler.class);
190         thingHandler = addThing(thing, thingClass);
191
192         createAndLinkItems();
193         assertThatAllItemStatesAreNull();
194     }
195
196     @AfterEach
197     public void tearDown() {
198         servlet.reset();
199         servlet.closeConnections();
200
201         if (thing != null) {
202             managedThingProvider.remove(thing.getUID());
203         }
204         if (bridge != null) {
205             managedThingProvider.remove(bridge.getUID());
206         }
207
208         unregisterService(volatileStorageService);
209     }
210
211     protected Bridge buildBridge() {
212         Map<String, Object> properties = new HashMap<>();
213         properties.put(WWNAccountConfiguration.ACCESS_TOKEN,
214                 "c.eQ5QBBPiFOTNzPHbmZPcE9yPZ7GayzLusifgQR2DQRFNyUS9ESvlhJF0D7vG8Y0TFV39zX1vIOsWrv8RKCMrFepNUb9FqHEboa4MtWLUsGb4tD9oBh0jrV4HooJUmz5sVA5KZR0dkxyLYyPc");
215         properties.put(WWNAccountConfiguration.PINCODE, "64P2XRYT");
216         properties.put(WWNAccountConfiguration.PRODUCT_ID, "8fdf9885-ca07-4252-1aa3-f3d5ca9589e0");
217         properties.put(WWNAccountConfiguration.PRODUCT_SECRET, "QITLR3iyUlWaj9dbvCxsCKp4f");
218
219         return BridgeBuilder.create(WWNTestAccountHandler.THING_TYPE_TEST_BRIDGE, "test_account")
220                 .withLabel("Test Account").withConfiguration(new Configuration(properties)).build();
221     }
222
223     protected abstract Thing buildThing(Bridge bridge);
224
225     protected List<Channel> buildChannels(ThingTypeUID thingTypeUID, ThingUID thingUID) {
226         waitForAssert(() -> assertThat(thingTypeRegistry.getThingType(thingTypeUID), notNullValue()));
227
228         ThingType thingType = thingTypeRegistry.getThingType(thingTypeUID);
229
230         List<Channel> channels = new ArrayList<>();
231         channels.addAll(buildChannels(thingUID, thingType.getChannelDefinitions(), (id) -> id));
232
233         for (ChannelGroupDefinition channelGroupDefinition : thingType.getChannelGroupDefinitions()) {
234             ChannelGroupType channelGroupType = channelGroupTypeRegistry
235                     .getChannelGroupType(channelGroupDefinition.getTypeUID());
236             String groupId = channelGroupDefinition.getId();
237             if (channelGroupType != null) {
238                 channels.addAll(
239                         buildChannels(thingUID, channelGroupType.getChannelDefinitions(), (id) -> groupId + "#" + id));
240             }
241         }
242
243         channels.sort((Channel c1, Channel c2) -> c1.getUID().getId().compareTo(c2.getUID().getId()));
244         return channels;
245     }
246
247     protected List<Channel> buildChannels(ThingUID thingUID, List<ChannelDefinition> channelDefinitions,
248             Function<String, String> channelIdFunction) {
249         List<Channel> result = new ArrayList<>();
250         for (ChannelDefinition channelDefinition : channelDefinitions) {
251             ChannelType channelType = channelTypeRegistry.getChannelType(channelDefinition.getChannelTypeUID());
252             if (channelType != null) {
253                 result.add(ChannelBuilder
254                         .create(new ChannelUID(thingUID, channelIdFunction.apply(channelDefinition.getId())),
255                                 channelType.getItemType())
256                         .build());
257             }
258         }
259         return result;
260     }
261
262     @SuppressWarnings("unchecked")
263     protected <T> T addThing(Thing thing, Class<T> thingHandlerClass) {
264         assertThat(thing.getHandler(), is(nullValue()));
265         managedThingProvider.add(thing);
266         waitForAssert(() -> assertThat(thing.getHandler(), notNullValue()));
267         assertThat(thing.getConfiguration(), is(notNullValue()));
268         assertThat(thing.getHandler(), is(instanceOf(thingHandlerClass)));
269         return (T) thing.getHandler();
270     }
271
272     protected String getThingId() {
273         return thing.getUID().getId();
274     }
275
276     protected ThingUID getThingUID() {
277         return thing.getUID();
278     }
279
280     protected void putStreamingEventData(String json) throws IOException {
281         String singleLineJson = json.replaceAll("\n\r\\s+", "").replaceAll("\n\\s+", "").replaceAll("\n\r", "")
282                 .replaceAll("\n", "");
283         servlet.queueEvent(PUT, singleLineJson);
284     }
285
286     protected void createAndLinkItems() {
287         thing.getChannels().forEach(c -> {
288             String itemName = getItemName(c.getUID().getId());
289             Item item = itemFactory.createItem(c.getAcceptedItemType(), itemName);
290             if (item != null) {
291                 itemRegistry.add(item);
292             }
293             managedItemChannelLinkProvider.add(new ItemChannelLink(itemName, c.getUID()));
294         });
295     }
296
297     protected void assertThatItemHasState(String channelId, State state) {
298         waitForAssert(() -> assertThat("Wrong state for item of channel '" + channelId + "' ", getItemState(channelId),
299                 is(state)));
300     }
301
302     protected void assertThatItemHasNotState(String channelId, State state) {
303         waitForAssert(() -> assertThat("Wrong state for item of channel '" + channelId + "' ", getItemState(channelId),
304                 is(not(state))));
305     }
306
307     protected void assertThatAllItemStatesAreNull() {
308         thing.getChannels().forEach(c -> assertThatItemHasState(c.getUID().getId(), UnDefType.NULL));
309     }
310
311     protected void assertThatAllItemStatesAreNotNull() {
312         thing.getChannels().forEach(c -> assertThatItemHasNotState(c.getUID().getId(), UnDefType.NULL));
313     }
314
315     protected ChannelUID getChannelUID(String channelId) {
316         return new ChannelUID(getThingUID(), channelId);
317     }
318
319     protected String getItemName(String channelId) {
320         return getThingId() + "_" + channelId.replaceAll("#", "_");
321     }
322
323     private State getItemState(String channelId) {
324         String itemName = getItemName(channelId);
325         try {
326             return itemRegistry.getItem(itemName).getState();
327         } catch (ItemNotFoundException e) {
328             throw new AssertionError("Item with name '" + itemName + "' not found");
329         }
330     }
331
332     protected void logItemStates() {
333         thing.getChannels().forEach(c -> {
334             String channelId = c.getUID().getId();
335             String itemName = getItemName(channelId);
336             logger.debug("{} = {}", itemName, getItemState(channelId));
337         });
338     }
339
340     protected void updateAllItemStatesToNull() {
341         thing.getChannels().forEach(c -> updateItemState(c.getUID().getId(), UnDefType.NULL));
342     }
343
344     protected void refreshAllChannels() {
345         thing.getChannels().forEach(c -> thingHandler.handleCommand(c.getUID(), RefreshType.REFRESH));
346     }
347
348     protected void handleCommand(String channelId, Command command) {
349         thingHandler.handleCommand(getChannelUID(channelId), command);
350     }
351
352     protected void updateItemState(String channelId, State state) {
353         String itemName = getItemName(channelId);
354         eventPublisher.post(ItemEventFactory.createStateEvent(itemName, state));
355     }
356
357     protected void assertNestApiPropertyState(String nestId, String propertyName, String state) {
358         waitForAssert(() -> assertThat(servlet.getNestIdPropertyState(nestId, propertyName), is(state)));
359     }
360
361     public static DateTimeType parseDateTimeType(String text) {
362         try {
363             return new DateTimeType(Instant.parse(text).atZone(TimeZone.getDefault().toZoneId()));
364         } catch (DateTimeParseException e) {
365             throw new IllegalArgumentException("Invalid date time argument: " + text, e);
366         }
367     }
368 }