]> git.basschouten.com Git - openhab-addons.git/blob
65c5e3ae2a814e06da9a1446a5883774b4b8f62b
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.NonNullByDefault;
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.LENIENT)
63 @NonNullByDefault
64 public class MqttTopicClassMapperTests {
65     @Retention(RetentionPolicy.RUNTIME)
66     @Target({ FIELD })
67     private @interface TestValue {
68         String value() default "";
69     }
70
71     @TopicPrefix
72     public static class Attributes extends AbstractMqttAttributeClass {
73         public transient String ignoreTransient = "";
74         public final String ignoreFinal = "";
75
76         public @TestValue("string") @Nullable String aString;
77         public @TestValue("false") @Nullable Boolean aBoolean;
78         public @TestValue("10") @Nullable Long aLong;
79         public @TestValue("10") @Nullable Integer aInteger;
80         public @TestValue("10") @Nullable BigDecimal aDecimal;
81
82         public @TestValue("10") @TopicPrefix("a") int aInt = 24;
83         public @TestValue("false") boolean aBool = true;
84         public @TestValue("abc,def") @MQTTvalueTransform(splitCharacter = ",") String @Nullable [] properties;
85
86         public enum ReadyState {
87             unknown,
88             init,
89             ready,
90         }
91
92         public @TestValue("init") ReadyState state = ReadyState.unknown;
93
94         public enum DataTypeEnum {
95             unknown,
96             integer_,
97             float_,
98         }
99
100         public @TestValue("integer") @MQTTvalueTransform(suffix = "_") DataTypeEnum datatype = DataTypeEnum.unknown;
101
102         @Override
103         public Object getFieldsOf() {
104             return this;
105         }
106     }
107
108     private @Mock @NonNullByDefault({}) MqttBrokerConnection connectionMock;
109     private @Mock @NonNullByDefault({}) ScheduledExecutorService executorMock;
110     private @Mock @NonNullByDefault({}) AttributeChanged fieldChangedObserverMock;
111     private @Spy Object countInjectedFields = new Object();
112
113     int injectedFields = 0;
114
115     // A completed future is returned for a subscribe call to the attributes
116     final CompletableFuture<Boolean> future = CompletableFuture.completedFuture(true);
117
118     @BeforeEach
119     public void setUp() {
120         doReturn(CompletableFuture.completedFuture(true)).when(connectionMock).subscribe(any(), any());
121         doReturn(CompletableFuture.completedFuture(true)).when(connectionMock).unsubscribe(any(), any());
122         injectedFields = (int) Stream.of(countInjectedFields.getClass().getDeclaredFields())
123                 .filter(AbstractMqttAttributeClass::filterField).count();
124     }
125
126     public Object createSubscriberAnswer(InvocationOnMock invocation) {
127         final AbstractMqttAttributeClass attributes = (AbstractMqttAttributeClass) invocation.getMock();
128         final ScheduledExecutorService scheduler = (ScheduledExecutorService) invocation.getArguments()[0];
129         final Field field = (Field) invocation.getArguments()[1];
130         final String topic = (String) invocation.getArguments()[2];
131         final boolean mandatory = (boolean) invocation.getArguments()[3];
132         final SubscribeFieldToMQTTtopic s = spy(
133                 new SubscribeFieldToMQTTtopic(scheduler, field, attributes, topic, mandatory));
134         doReturn(CompletableFuture.completedFuture(true)).when(s).subscribeAndReceive(any(), anyInt());
135         return s;
136     }
137
138     @Test
139     public void subscribeToCorrectFields() {
140         Attributes attributes = spy(new Attributes());
141
142         doAnswer(this::createSubscriberAnswer).when(attributes).createSubscriber(any(), any(), anyString(),
143                 anyBoolean());
144
145         // Subscribe now to all fields
146         CompletableFuture<@Nullable Void> future = attributes.subscribeAndReceive(connectionMock, executorMock,
147                 "homie/device123", null, 10);
148         assertThat(future.isDone(), is(true));
149         assertThat(attributes.subscriptions.size(), is(10 + injectedFields));
150     }
151
152     // TODO timeout
153     @SuppressWarnings({ "null", "unused" })
154     @Test
155     public void subscribeAndReceive() throws Exception {
156         final Attributes attributes = spy(new Attributes());
157
158         doAnswer(this::createSubscriberAnswer).when(attributes).createSubscriber(any(), any(), anyString(),
159                 anyBoolean());
160
161         verify(connectionMock, times(0)).subscribe(anyString(), any());
162
163         // Subscribe now to all fields
164         CompletableFuture<@Nullable Void> future = attributes.subscribeAndReceive(connectionMock, executorMock,
165                 "homie/device123", fieldChangedObserverMock, 10);
166         assertThat(future.isDone(), is(true));
167
168         // We expect 10 subscriptions now
169         assertThat(attributes.subscriptions.size(), is(10 + injectedFields));
170
171         int loopCounter = 0;
172
173         // Assign each field the value of the test annotation via the processMessage method
174         for (SubscribeFieldToMQTTtopic f : attributes.subscriptions) {
175             @Nullable
176             TestValue annotation = f.field.getAnnotation(TestValue.class);
177             // A non-annotated field means a Mockito injected field.
178             // Ignore that and complete the corresponding future.
179             if (annotation == null) {
180                 f.future.complete(null);
181                 continue;
182             }
183
184             verify(f).subscribeAndReceive(any(), anyInt());
185
186             // Simulate a received MQTT value and use the annotation data as input.
187             f.processMessage(f.topic, annotation.value().getBytes());
188             verify(fieldChangedObserverMock, times(++loopCounter)).attributeChanged(any(), any(), any(), any(),
189                     anyBoolean());
190
191             // Check each value if the assignment worked
192             if (!f.field.getType().isArray()) {
193                 assertNotNull(f.field.get(attributes), f.field.getName() + " is null");
194                 // Consider if a mapToField was used that would manipulate the received value
195                 MQTTvalueTransform mapToField = f.field.getAnnotation(MQTTvalueTransform.class);
196                 String prefix = mapToField != null ? mapToField.prefix() : "";
197                 String suffix = mapToField != null ? mapToField.suffix() : "";
198                 assertThat(f.field.get(attributes).toString(), is(prefix + annotation.value() + suffix));
199             } else {
200                 String[] attributeArray = (String[]) f.field.get(attributes);
201                 assertNotNull(attributeArray);
202                 Objects.requireNonNull(attributeArray);
203                 assertThat(Stream.of(attributeArray).reduce((v, i) -> v + "," + i).orElse(""), is(annotation.value()));
204             }
205         }
206
207         assertThat(future.isDone(), is(true));
208     }
209
210     @Test
211     public void ignoresInvalidEnum() throws Exception {
212         final Attributes attributes = spy(new Attributes());
213
214         doAnswer(this::createSubscriberAnswer).when(attributes).createSubscriber(any(), any(), anyString(),
215                 anyBoolean());
216
217         verify(connectionMock, times(0)).subscribe(anyString(), any());
218
219         // Subscribe now to all fields
220         CompletableFuture<@Nullable Void> future = attributes.subscribeAndReceive(connectionMock, executorMock,
221                 "homie/device123", fieldChangedObserverMock, 10);
222         assertThat(future.isDone(), is(true));
223
224         SubscribeFieldToMQTTtopic field = attributes.subscriptions.stream().filter(f -> f.field.getName() == "state")
225                 .findFirst().get();
226         field.processMessage(field.topic, "garbage".getBytes());
227         verify(fieldChangedObserverMock, times(0)).attributeChanged(any(), any(), any(), any(), anyBoolean());
228         assertThat(attributes.state.toString(), is("unknown"));
229     }
230 }