2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.nest.internal.wwn.handler;
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;
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;
31 import java.util.Objects;
32 import java.util.TimeZone;
33 import java.util.function.Function;
35 import javax.ws.rs.client.ClientBuilder;
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;
93 * {@link WWNThingHandlerOSGiTest} is an abstract base class for Nest OSGi based tests.
95 * @author Wouter Born - Initial contribution
98 public abstract class WWNThingHandlerOSGiTest extends JavaOSGiTest {
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;
105 private final Logger logger = LoggerFactory.getLogger(WWNThingHandlerOSGiTest.class);
107 private static @Nullable WWNTestServer server;
108 private static WWNTestApiServlet servlet = new WWNTestApiServlet();
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();
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;
126 private @NonNullByDefault({}) WWNTestHandlerFactory nestTestHandlerFactory;
127 private @NonNullByDefault({}) ClientBuilder clientBuilder;
128 private @NonNullByDefault({}) SseEventSourceFactory eventSourceFactory;
130 public WWNThingHandlerOSGiTest(Class<? extends WWNBaseHandler<?>> thingClass) {
131 this.thingClass = thingClass;
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();
142 public static void tearDownClass() throws Exception {
143 WWNTestServer testServer = server;
144 if (testServer != null) {
145 testServer.stopServer();
150 public void setUp() throws ItemNotFoundException {
151 registerService(volatileStorageService);
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");
170 ComponentContext componentContext = mock(ComponentContext.class);
171 when(componentContext.getBundleContext()).thenReturn(bundleContext);
173 nestTestHandlerFactory = new WWNTestHandlerFactory(clientBuilder, eventSourceFactory);
174 nestTestHandlerFactory.activate(componentContext,
175 Map.of(WWNTestHandlerFactory.REDIRECT_URL_CONFIG_PROPERTY, REDIRECT_URL));
176 registerService(nestTestHandlerFactory);
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);
183 nestTestHandlerFactory = Objects.requireNonNull(
184 getService(ThingHandlerFactory.class, WWNTestHandlerFactory.class),
185 "Could not get NestTestHandlerFactory");
187 bridge = buildBridge();
188 thing = buildThing(bridge);
190 bridgeHandler = addThing(bridge, WWNTestAccountHandler.class);
191 thingHandler = addThing(thing, thingClass);
193 createAndLinkItems();
194 assertThatAllItemStatesAreNull();
198 public void tearDown() {
200 servlet.closeConnections();
203 managedThingProvider.remove(thing.getUID());
205 if (bridge != null) {
206 managedThingProvider.remove(bridge.getUID());
209 unregisterService(volatileStorageService);
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"));
220 return BridgeBuilder.create(WWNTestAccountHandler.THING_TYPE_TEST_BRIDGE, "test_account")
221 .withLabel("Test Account").withConfiguration(new Configuration(properties)).build();
224 protected abstract Thing buildThing(Bridge bridge);
226 protected List<Channel> buildChannels(ThingTypeUID thingTypeUID, ThingUID thingUID) {
227 waitForAssert(() -> assertThat(thingTypeRegistry.getThingType(thingTypeUID), notNullValue()));
229 ThingType thingType = thingTypeRegistry.getThingType(thingTypeUID);
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) {
238 buildChannels(thingUID, channelGroupType.getChannelDefinitions(), id -> groupId + "#" + id));
242 channels.sort((Channel c1, Channel c2) -> c1.getUID().getId().compareTo(c2.getUID().getId()));
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())
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();
271 protected String getThingId() {
272 return thing.getUID().getId();
275 protected ThingUID getThingUID() {
276 return thing.getUID();
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);
285 protected void createAndLinkItems() {
286 thing.getChannels().forEach(c -> {
287 String itemName = getItemName(c.getUID().getId());
288 Item item = itemFactory.createItem(c.getAcceptedItemType(), itemName);
290 itemRegistry.add(item);
292 managedItemChannelLinkProvider.add(new ItemChannelLink(itemName, c.getUID()));
296 protected void assertThatItemHasState(String channelId, State state) {
297 waitForAssert(() -> assertThat("Wrong state for item of channel '" + channelId + "' ", getItemState(channelId),
301 protected void assertThatItemHasNotState(String channelId, State state) {
302 waitForAssert(() -> assertThat("Wrong state for item of channel '" + channelId + "' ", getItemState(channelId),
306 protected void assertThatAllItemStatesAreNull() {
307 thing.getChannels().forEach(c -> assertThatItemHasState(c.getUID().getId(), UnDefType.NULL));
310 protected void assertThatAllItemStatesAreNotNull() {
311 thing.getChannels().forEach(c -> assertThatItemHasNotState(c.getUID().getId(), UnDefType.NULL));
314 protected ChannelUID getChannelUID(String channelId) {
315 return new ChannelUID(getThingUID(), channelId);
318 protected String getItemName(String channelId) {
319 return getThingId() + "_" + channelId.replaceAll("#", "_");
322 private State getItemState(String channelId) {
323 String itemName = getItemName(channelId);
325 return itemRegistry.getItem(itemName).getState();
326 } catch (ItemNotFoundException e) {
327 throw new AssertionError("Item with name '" + itemName + "' not found");
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));
339 protected void updateAllItemStatesToNull() {
340 thing.getChannels().forEach(c -> updateItemState(c.getUID().getId(), UnDefType.NULL));
343 protected void refreshAllChannels() {
344 thing.getChannels().forEach(c -> thingHandler.handleCommand(c.getUID(), RefreshType.REFRESH));
347 protected void handleCommand(String channelId, Command command) {
348 thingHandler.handleCommand(getChannelUID(channelId), command);
351 protected void updateItemState(String channelId, State state) {
352 String itemName = getItemName(channelId);
353 eventPublisher.post(ItemEventFactory.createStateEvent(itemName, state));
356 protected void assertNestApiPropertyState(String nestId, String propertyName, String state) {
357 waitForAssert(() -> assertThat(servlet.getNestIdPropertyState(nestId, propertyName), is(state)));
360 public static DateTimeType parseDateTimeType(String text) {
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);