]> git.basschouten.com Git - openhab-addons.git/blob
d032c80481169f1d1f03d28a81854b6b06f71541
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.mqtt.generic.mapping;
14
15 import static java.lang.annotation.ElementType.FIELD;
16 import static org.hamcrest.CoreMatchers.is;
17 import static org.junit.Assert.*;
18 import static org.mockito.ArgumentMatchers.*;
19 import static org.mockito.Mockito.*;
20
21 import java.lang.annotation.Retention;
22 import java.lang.annotation.RetentionPolicy;
23 import java.lang.annotation.Target;
24 import java.lang.reflect.Field;
25 import java.math.BigDecimal;
26 import java.util.concurrent.CompletableFuture;
27 import java.util.concurrent.ScheduledExecutorService;
28 import java.util.stream.Stream;
29
30 import org.eclipse.jdt.annotation.NonNull;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.junit.Before;
33 import org.junit.Test;
34 import org.mockito.Mock;
35 import org.mockito.MockitoAnnotations;
36 import org.mockito.Spy;
37 import org.mockito.invocation.InvocationOnMock;
38 import org.openhab.binding.mqtt.generic.mapping.AbstractMqttAttributeClass.AttributeChanged;
39 import org.openhab.core.io.transport.mqtt.MqttBrokerConnection;
40
41 /**
42  * Tests cases for {@link org.openhab.binding.mqtt.generic.mapping.AbstractMqttAttributeClass}.
43  *
44  * <p>
45  * How it works:
46  *
47  * <ol>
48  * <li>A DTO (data transfer object) is defined, here it is {@link Attributes}, which extends
49  * {@link org.openhab.binding.mqtt.generic.mapping.AbstractMqttAttributeClass}.
50  * <li>The createSubscriber method is mocked so that no real MQTTConnection interaction happens.
51  * <li>The subscribeAndReceive method is called.
52  * </ol>
53  *
54  * @author David Graeff - Initial contribution
55  */
56 public class MqttTopicClassMapperTests {
57     @Retention(RetentionPolicy.RUNTIME)
58     @Target({ FIELD })
59     private @interface TestValue {
60         String value() default "";
61     }
62
63     @TopicPrefix
64     public static class Attributes extends AbstractMqttAttributeClass {
65         public transient String ignoreTransient = "";
66         public final String ignoreFinal = "";
67
68         public @TestValue("string") String aString;
69         public @TestValue("false") Boolean aBoolean;
70         public @TestValue("10") Long aLong;
71         public @TestValue("10") Integer aInteger;
72         public @TestValue("10") BigDecimal aDecimal;
73
74         public @TestValue("10") @TopicPrefix("a") int Int = 24;
75         public @TestValue("false") boolean aBool = true;
76         public @TestValue("abc,def") @MQTTvalueTransform(splitCharacter = ",") String[] properties;
77
78         public enum ReadyState {
79             unknown,
80             init,
81             ready,
82         }
83
84         public @TestValue("init") ReadyState state = ReadyState.unknown;
85
86         public enum DataTypeEnum {
87             unknown,
88             integer_,
89             float_,
90         }
91
92         public @TestValue("integer") @MQTTvalueTransform(suffix = "_") DataTypeEnum datatype = DataTypeEnum.unknown;
93
94         @Override
95         public @NonNull Object getFieldsOf() {
96             return this;
97         }
98     }
99
100     @Mock
101     MqttBrokerConnection connection;
102
103     @Mock
104     ScheduledExecutorService executor;
105
106     @Mock
107     AttributeChanged fieldChangedObserver;
108
109     @Spy
110     Object countInjectedFields = new Object();
111     int injectedFields = 0;
112
113     // A completed future is returned for a subscribe call to the attributes
114     final CompletableFuture<Boolean> future = CompletableFuture.completedFuture(true);
115
116     @Before
117     public void setUp() {
118         MockitoAnnotations.initMocks(this);
119         doReturn(CompletableFuture.completedFuture(true)).when(connection).subscribe(any(), any());
120         doReturn(CompletableFuture.completedFuture(true)).when(connection).unsubscribe(any(), any());
121         injectedFields = (int) Stream.of(countInjectedFields.getClass().getDeclaredFields())
122                 .filter(AbstractMqttAttributeClass::filterField).count();
123     }
124
125     public Object createSubscriberAnswer(InvocationOnMock invocation) {
126         final AbstractMqttAttributeClass attributes = (AbstractMqttAttributeClass) invocation.getMock();
127         final ScheduledExecutorService scheduler = (ScheduledExecutorService) invocation.getArguments()[0];
128         final Field field = (Field) invocation.getArguments()[1];
129         final String topic = (String) invocation.getArguments()[2];
130         final boolean mandatory = (boolean) invocation.getArguments()[3];
131         final SubscribeFieldToMQTTtopic s = spy(
132                 new SubscribeFieldToMQTTtopic(scheduler, field, attributes, topic, mandatory));
133         doReturn(CompletableFuture.completedFuture(true)).when(s).subscribeAndReceive(any(), anyInt());
134         return s;
135     }
136
137     @Test
138     public void subscribeToCorrectFields() {
139         Attributes attributes = spy(new Attributes());
140
141         doAnswer(this::createSubscriberAnswer).when(attributes).createSubscriber(any(), any(), anyString(),
142                 anyBoolean());
143
144         // Subscribe now to all fields
145         CompletableFuture<Void> future = attributes.subscribeAndReceive(connection, executor, "homie/device123", null,
146                 10);
147         assertThat(future.isDone(), is(true));
148         assertThat(attributes.subscriptions.size(), is(10 + injectedFields));
149     }
150
151     // TODO timeout
152     @SuppressWarnings({ "null", "unused" })
153     @Test
154     public void subscribeAndReceive() throws IllegalArgumentException, IllegalAccessException {
155         final Attributes attributes = spy(new Attributes());
156
157         doAnswer(this::createSubscriberAnswer).when(attributes).createSubscriber(any(), any(), anyString(),
158                 anyBoolean());
159
160         verify(connection, times(0)).subscribe(anyString(), any());
161
162         // Subscribe now to all fields
163         CompletableFuture<Void> future = attributes.subscribeAndReceive(connection, executor, "homie/device123",
164                 fieldChangedObserver, 10);
165         assertThat(future.isDone(), is(true));
166
167         // We expect 10 subscriptions now
168         assertThat(attributes.subscriptions.size(), is(10 + injectedFields));
169
170         int loopCounter = 0;
171
172         // Assign each field the value of the test annotation via the processMessage method
173         for (SubscribeFieldToMQTTtopic f : attributes.subscriptions) {
174             @Nullable
175             TestValue annotation = f.field.getAnnotation(TestValue.class);
176             // A non-annotated field means a Mockito injected field.
177             // Ignore that and complete the corresponding future.
178             if (annotation == null) {
179                 f.future.complete(null);
180                 continue;
181             }
182
183             verify(f).subscribeAndReceive(any(), anyInt());
184
185             // Simulate a received MQTT value and use the annotation data as input.
186             f.processMessage(f.topic, annotation.value().getBytes());
187             verify(fieldChangedObserver, times(++loopCounter)).attributeChanged(any(), any(), any(), any(),
188                     anyBoolean());
189
190             // Check each value if the assignment worked
191             if (!f.field.getType().isArray()) {
192                 assertNotNull(f.field.getName() + " is null", f.field.get(attributes));
193                 // Consider if a mapToField was used that would manipulate the received value
194                 MQTTvalueTransform mapToField = f.field.getAnnotation(MQTTvalueTransform.class);
195                 String prefix = mapToField != null ? mapToField.prefix() : "";
196                 String suffix = mapToField != null ? mapToField.suffix() : "";
197                 assertThat(f.field.get(attributes).toString(), is(prefix + annotation.value() + suffix));
198             } else {
199                 assertThat(Stream.of((String[]) f.field.get(attributes)).reduce((v, i) -> v + "," + i).orElse(""),
200                         is(annotation.value()));
201             }
202         }
203
204         assertThat(future.isDone(), is(true));
205     }
206 }