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.mqtt.generic.mapping;
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.*;
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;
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;
47 * Tests cases for {@link org.openhab.binding.mqtt.generic.mapping.AbstractMqttAttributeClass}.
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.
59 * @author David Graeff - Initial contribution
61 @ExtendWith(MockitoExtension.class)
62 @MockitoSettings(strictness = Strictness.LENIENT)
64 public class MqttTopicClassMapperTests {
65 @Retention(RetentionPolicy.RUNTIME)
67 private @interface TestValue {
68 String value() default "";
72 public static class Attributes extends AbstractMqttAttributeClass {
73 public transient String ignoreTransient = "";
74 public final String ignoreFinal = "";
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;
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;
86 public enum ReadyState {
92 public @TestValue("init") ReadyState state = ReadyState.unknown;
94 public enum DataTypeEnum {
100 public @TestValue("integer") @MQTTvalueTransform(suffix = "_") DataTypeEnum datatype = DataTypeEnum.unknown;
103 public Object getFieldsOf() {
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();
113 int injectedFields = 0;
115 // A completed future is returned for a subscribe call to the attributes
116 final CompletableFuture<Boolean> future = CompletableFuture.completedFuture(true);
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();
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());
139 public void subscribeToCorrectFields() {
140 Attributes attributes = spy(new Attributes());
142 doAnswer(this::createSubscriberAnswer).when(attributes).createSubscriber(any(), any(), anyString(),
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));
153 @SuppressWarnings({ "null", "unused" })
155 public void subscribeAndReceive() throws Exception {
156 final Attributes attributes = spy(new Attributes());
158 doAnswer(this::createSubscriberAnswer).when(attributes).createSubscriber(any(), any(), anyString(),
161 verify(connectionMock, times(0)).subscribe(anyString(), any());
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));
168 // We expect 10 subscriptions now
169 assertThat(attributes.subscriptions.size(), is(10 + injectedFields));
173 // Assign each field the value of the test annotation via the processMessage method
174 for (SubscribeFieldToMQTTtopic f : attributes.subscriptions) {
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);
184 verify(f).subscribeAndReceive(any(), anyInt());
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(),
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));
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()));
207 assertThat(future.isDone(), is(true));
211 public void ignoresInvalidEnum() throws Exception {
212 final Attributes attributes = spy(new Attributes());
214 doAnswer(this::createSubscriberAnswer).when(attributes).createSubscriber(any(), any(), anyString(),
217 verify(connectionMock, times(0)).subscribe(anyString(), any());
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));
224 SubscribeFieldToMQTTtopic field = attributes.subscriptions.stream().filter(f -> f.field.getName() == "state")
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"));