Commit 69a77792530de72ab27a437def806702d032df75
Committed by
Andrew Shvayka
1 parent
bc7cf029
added MqttTransportHandlerTest
Showing
1 changed file
with
219 additions
and
0 deletions
1 | +/** | ||
2 | + * Copyright © 2016-2021 The Thingsboard Authors | ||
3 | + * | ||
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | ||
5 | + * you may not use this file except in compliance with the License. | ||
6 | + * You may obtain a copy of the License at | ||
7 | + * | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * | ||
10 | + * Unless required by applicable law or agreed to in writing, software | ||
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | ||
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
13 | + * See the License for the specific language governing permissions and | ||
14 | + * limitations under the License. | ||
15 | + */ | ||
16 | +package org.thingsboard.server.transport.mqtt; | ||
17 | + | ||
18 | +import io.netty.buffer.ByteBuf; | ||
19 | +import io.netty.buffer.EmptyByteBuf; | ||
20 | +import io.netty.buffer.PooledByteBufAllocator; | ||
21 | +import io.netty.channel.ChannelHandlerContext; | ||
22 | +import io.netty.handler.codec.mqtt.MqttConnectMessage; | ||
23 | +import io.netty.handler.codec.mqtt.MqttConnectPayload; | ||
24 | +import io.netty.handler.codec.mqtt.MqttConnectVariableHeader; | ||
25 | +import io.netty.handler.codec.mqtt.MqttFixedHeader; | ||
26 | +import io.netty.handler.codec.mqtt.MqttMessage; | ||
27 | +import io.netty.handler.codec.mqtt.MqttMessageType; | ||
28 | +import io.netty.handler.codec.mqtt.MqttPublishMessage; | ||
29 | +import io.netty.handler.codec.mqtt.MqttPublishVariableHeader; | ||
30 | +import io.netty.handler.codec.mqtt.MqttQoS; | ||
31 | +import io.netty.handler.ssl.SslHandler; | ||
32 | +import lombok.extern.slf4j.Slf4j; | ||
33 | +import org.junit.After; | ||
34 | +import org.junit.Before; | ||
35 | +import org.junit.Test; | ||
36 | +import org.junit.runner.RunWith; | ||
37 | +import org.mockito.Mock; | ||
38 | +import org.mockito.junit.MockitoJUnitRunner; | ||
39 | +import org.thingsboard.common.util.ThingsBoardThreadFactory; | ||
40 | + | ||
41 | +import java.net.InetSocketAddress; | ||
42 | +import java.nio.charset.StandardCharsets; | ||
43 | +import java.util.List; | ||
44 | +import java.util.concurrent.CountDownLatch; | ||
45 | +import java.util.concurrent.ExecutorService; | ||
46 | +import java.util.concurrent.Executors; | ||
47 | +import java.util.concurrent.TimeUnit; | ||
48 | +import java.util.concurrent.atomic.AtomicInteger; | ||
49 | +import java.util.stream.Collectors; | ||
50 | +import java.util.stream.Stream; | ||
51 | + | ||
52 | +import static org.hamcrest.MatcherAssert.assertThat; | ||
53 | +import static org.hamcrest.Matchers.contains; | ||
54 | +import static org.hamcrest.Matchers.empty; | ||
55 | +import static org.hamcrest.Matchers.greaterThan; | ||
56 | +import static org.hamcrest.Matchers.is; | ||
57 | +import static org.junit.Assert.fail; | ||
58 | +import static org.mockito.ArgumentMatchers.any; | ||
59 | +import static org.mockito.BDDMockito.willDoNothing; | ||
60 | +import static org.mockito.BDDMockito.willReturn; | ||
61 | +import static org.mockito.Mockito.never; | ||
62 | +import static org.mockito.Mockito.spy; | ||
63 | +import static org.mockito.Mockito.times; | ||
64 | +import static org.mockito.Mockito.verify; | ||
65 | + | ||
66 | +@Slf4j | ||
67 | +@RunWith(MockitoJUnitRunner.class) | ||
68 | +public class MqttTransportHandlerTest { | ||
69 | + | ||
70 | + public static final int MSG_QUEUE_LIMIT = 10; | ||
71 | + public static final InetSocketAddress IP_ADDR = new InetSocketAddress("127.0.0.1", 9876); | ||
72 | + public static final int TIMEOUT = 30; | ||
73 | + | ||
74 | + @Mock | ||
75 | + MqttTransportContext context; | ||
76 | + @Mock | ||
77 | + SslHandler sslHandler; | ||
78 | + @Mock | ||
79 | + ChannelHandlerContext ctx; | ||
80 | + | ||
81 | + AtomicInteger packedId = new AtomicInteger(); | ||
82 | + ExecutorService executor; | ||
83 | + MqttTransportHandler handler; | ||
84 | + | ||
85 | + @Before | ||
86 | + public void setUp() throws Exception { | ||
87 | + willReturn(MSG_QUEUE_LIMIT).given(context).getMessageQueueSizePerDeviceLimit(); | ||
88 | + | ||
89 | + handler = spy(new MqttTransportHandler(context, sslHandler)); | ||
90 | + willReturn(IP_ADDR).given(handler).getAddress(any()); | ||
91 | + } | ||
92 | + | ||
93 | + @After | ||
94 | + public void tearDown() { | ||
95 | + if (executor != null) { | ||
96 | + executor.shutdownNow(); | ||
97 | + } | ||
98 | + } | ||
99 | + | ||
100 | + @Test | ||
101 | + public void givenMessageWithoutFixedHeader_whenProcessMqttMsg_thenProcessDisconnect() { | ||
102 | + MqttFixedHeader mqttFixedHeader = null; | ||
103 | + MqttMessage msg = new MqttMessage(mqttFixedHeader); | ||
104 | + willDoNothing().given(handler).processDisconnect(ctx); | ||
105 | + | ||
106 | + handler.processMqttMsg(ctx, msg); | ||
107 | + | ||
108 | + assertThat(handler.address, is(IP_ADDR)); | ||
109 | + verify(handler, times(1)).processDisconnect(ctx); | ||
110 | + } | ||
111 | + | ||
112 | + MqttConnectMessage getMqttConnectMessage() { | ||
113 | + MqttFixedHeader mqttFixedHeader = new MqttFixedHeader(MqttMessageType.CONNECT, true, MqttQoS.AT_LEAST_ONCE, false, 123); | ||
114 | + MqttConnectVariableHeader variableHeader = new MqttConnectVariableHeader("device", packedId.incrementAndGet(), true, true, true, 1, true, false, 60); | ||
115 | + MqttConnectPayload payload = new MqttConnectPayload("clientId", "topic", "message".getBytes(StandardCharsets.UTF_8), "username", "password".getBytes(StandardCharsets.UTF_8)); | ||
116 | + return new MqttConnectMessage(mqttFixedHeader, variableHeader, payload); | ||
117 | + } | ||
118 | + | ||
119 | + MqttPublishMessage getMqttPublishMessage() { | ||
120 | + MqttFixedHeader mqttFixedHeader = new MqttFixedHeader(MqttMessageType.PUBLISH, true, MqttQoS.AT_LEAST_ONCE, false, 123); | ||
121 | + MqttPublishVariableHeader variableHeader = new MqttPublishVariableHeader("v1/gateway/telemetry", packedId.incrementAndGet()); | ||
122 | + ByteBuf payload = new EmptyByteBuf(new PooledByteBufAllocator()); | ||
123 | + return new MqttPublishMessage(mqttFixedHeader, variableHeader, payload); | ||
124 | + } | ||
125 | + | ||
126 | + @Test | ||
127 | + public void givenMqttConnectMessage_whenProcessMqttMsg_thenProcessConnect() { | ||
128 | + MqttConnectMessage msg = getMqttConnectMessage(); | ||
129 | + willDoNothing().given(handler).processConnect(ctx, msg); | ||
130 | + | ||
131 | + handler.processMqttMsg(ctx, msg); | ||
132 | + | ||
133 | + assertThat(handler.address, is(IP_ADDR)); | ||
134 | + assertThat(handler.deviceSessionCtx.getChannel(), is(ctx)); | ||
135 | + verify(handler, never()).processDisconnect(any()); | ||
136 | + verify(handler, times(1)).processConnect(ctx, msg); | ||
137 | + } | ||
138 | + | ||
139 | + @Test | ||
140 | + public void givenQueueLimit_whenEnqueueRegularSessionMsgOverLimit_thenOK() { | ||
141 | + List<MqttPublishMessage> messages = Stream.generate(this::getMqttPublishMessage).limit(MSG_QUEUE_LIMIT).collect(Collectors.toList()); | ||
142 | + messages.forEach(msg -> handler.enqueueRegularSessionMsg(ctx, msg)); | ||
143 | + assertThat(handler.deviceSessionCtx.getMsgQueueSize().get(), is(MSG_QUEUE_LIMIT)); | ||
144 | + assertThat(handler.deviceSessionCtx.getMsgQueue(), contains(messages.toArray())); | ||
145 | + } | ||
146 | + | ||
147 | + @Test | ||
148 | + public void givenQueueLimit_whenEnqueueRegularSessionMsgOverLimit_thenCtxClose() { | ||
149 | + final int limit = MSG_QUEUE_LIMIT + 1; | ||
150 | + willDoNothing().given(handler).processMsgQueue(ctx); | ||
151 | + List<MqttPublishMessage> messages = Stream.generate(this::getMqttPublishMessage).limit(limit).collect(Collectors.toList()); | ||
152 | + | ||
153 | + messages.forEach((msg) -> handler.enqueueRegularSessionMsg(ctx, msg)); | ||
154 | + | ||
155 | + assertThat(handler.deviceSessionCtx.getMsgQueueSize().get(), is(limit)); | ||
156 | + verify(handler, times(limit)).enqueueRegularSessionMsg(any(), any()); | ||
157 | + verify(handler, times(MSG_QUEUE_LIMIT)).processMsgQueue(any()); | ||
158 | + verify(ctx, times(1)).close(); | ||
159 | + } | ||
160 | + | ||
161 | + @Test | ||
162 | + public void givenMqttConnectMessageAndPublishImmediately_whenProcessMqttMsg_thenEnqueueRegularSessionMsg() { | ||
163 | + givenMqttConnectMessage_whenProcessMqttMsg_thenProcessConnect(); | ||
164 | + | ||
165 | + List<MqttPublishMessage> messages = Stream.generate(this::getMqttPublishMessage).limit(MSG_QUEUE_LIMIT).collect(Collectors.toList()); | ||
166 | + | ||
167 | + messages.forEach((msg) -> handler.processMqttMsg(ctx, msg)); | ||
168 | + | ||
169 | + assertThat(handler.address, is(IP_ADDR)); | ||
170 | + assertThat(handler.deviceSessionCtx.getChannel(), is(ctx)); | ||
171 | + assertThat(handler.deviceSessionCtx.getMsgQueueSize().get(), is(MSG_QUEUE_LIMIT)); | ||
172 | + assertThat(handler.deviceSessionCtx.getMsgQueue(), contains(messages.toArray())); | ||
173 | + verify(handler, never()).processDisconnect(any()); | ||
174 | + verify(handler, times(1)).processConnect(any(), any()); | ||
175 | + verify(handler, times(MSG_QUEUE_LIMIT)).enqueueRegularSessionMsg(any(), any()); | ||
176 | + messages.forEach((msg) -> verify(handler, times(1)).enqueueRegularSessionMsg(ctx, msg)); | ||
177 | + } | ||
178 | + | ||
179 | + @Test | ||
180 | + public void givenMessageQueue_whenProcessMqttMsg_thenEnqueueRegularSessionMsg() throws InterruptedException { | ||
181 | + //given | ||
182 | + assertThat(handler.deviceSessionCtx.isConnected(), is(false)); | ||
183 | + assertThat(MSG_QUEUE_LIMIT, greaterThan(2)); | ||
184 | + List<MqttPublishMessage> messages = Stream.generate(this::getMqttPublishMessage).limit(MSG_QUEUE_LIMIT).collect(Collectors.toList()); | ||
185 | + messages.forEach((msg) -> handler.enqueueRegularSessionMsg(ctx, msg)); | ||
186 | + willDoNothing().given(handler).processRegularSessionMsg(any(), any()); | ||
187 | + executor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName(getClass().getName())); | ||
188 | + | ||
189 | + CountDownLatch readyLatch = new CountDownLatch(MSG_QUEUE_LIMIT); | ||
190 | + CountDownLatch startLatch = new CountDownLatch(1); | ||
191 | + CountDownLatch finishLatch = new CountDownLatch(MSG_QUEUE_LIMIT); | ||
192 | + | ||
193 | + Stream.iterate(0, i -> i + 1).limit(MSG_QUEUE_LIMIT).forEach(x -> | ||
194 | + executor.submit(() -> { | ||
195 | + try { | ||
196 | + readyLatch.countDown(); | ||
197 | + assertThat(startLatch.await(TIMEOUT, TimeUnit.SECONDS), is(true)); | ||
198 | + handler.processMsgQueue(ctx); | ||
199 | + finishLatch.countDown(); | ||
200 | + } catch (Exception e) { | ||
201 | + log.error("Failed to run processMsgQueue", e); | ||
202 | + fail("Failed to run processMsgQueue"); | ||
203 | + } | ||
204 | + })); | ||
205 | + | ||
206 | + //when | ||
207 | + assertThat(readyLatch.await(TIMEOUT, TimeUnit.SECONDS), is(true)); | ||
208 | + handler.deviceSessionCtx.setConnected(true); | ||
209 | + startLatch.countDown(); | ||
210 | + assertThat(finishLatch.await(TIMEOUT, TimeUnit.SECONDS), is(true)); | ||
211 | + | ||
212 | + //then | ||
213 | + assertThat(handler.deviceSessionCtx.getMsgQueueSize().get(), is(0)); | ||
214 | + assertThat(handler.deviceSessionCtx.getMsgQueue(), empty()); | ||
215 | + verify(handler, times(MSG_QUEUE_LIMIT)).processRegularSessionMsg(any(), any()); | ||
216 | + messages.forEach((msg) -> verify(handler, times(1)).processRegularSessionMsg(ctx, msg)); | ||
217 | + } | ||
218 | + | ||
219 | +} |