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