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