Showing
16 changed files
with
1339 additions
and
424 deletions
@@ -304,6 +304,11 @@ | @@ -304,6 +304,11 @@ | ||
304 | <groupId>com.github.ua-parser</groupId> | 304 | <groupId>com.github.ua-parser</groupId> |
305 | <artifactId>uap-java</artifactId> | 305 | <artifactId>uap-java</artifactId> |
306 | </dependency> | 306 | </dependency> |
307 | + <dependency> | ||
308 | + <groupId>org.java-websocket</groupId> | ||
309 | + <artifactId>Java-WebSocket</artifactId> | ||
310 | + <scope>test</scope> | ||
311 | + </dependency> | ||
307 | </dependencies> | 312 | </dependencies> |
308 | 313 | ||
309 | <build> | 314 | <build> |
1 | +/** | ||
2 | + * Copyright © 2016-2020 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.service.subscription; | ||
17 | + | ||
18 | +import com.google.common.util.concurrent.Futures; | ||
19 | +import com.google.common.util.concurrent.ListenableFuture; | ||
20 | +import lombok.extern.slf4j.Slf4j; | ||
21 | +import org.springframework.beans.factory.annotation.Autowired; | ||
22 | +import org.springframework.context.annotation.Lazy; | ||
23 | +import org.springframework.context.event.EventListener; | ||
24 | +import org.springframework.stereotype.Service; | ||
25 | +import org.thingsboard.common.util.ThingsBoardThreadFactory; | ||
26 | +import org.thingsboard.server.common.data.EntityView; | ||
27 | +import org.thingsboard.server.common.data.id.CustomerId; | ||
28 | +import org.thingsboard.server.common.data.id.EntityViewId; | ||
29 | +import org.thingsboard.server.common.data.id.TenantId; | ||
30 | +import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; | ||
31 | +import org.thingsboard.server.common.data.kv.ReadTsKvQuery; | ||
32 | +import org.thingsboard.server.common.data.kv.TsKvEntry; | ||
33 | +import org.thingsboard.server.common.data.page.PageData; | ||
34 | +import org.thingsboard.server.common.data.query.EntityData; | ||
35 | +import org.thingsboard.server.common.data.query.EntityDataQuery; | ||
36 | +import org.thingsboard.server.common.data.query.TsValue; | ||
37 | +import org.thingsboard.server.common.msg.queue.ServiceType; | ||
38 | +import org.thingsboard.server.common.msg.queue.TbCallback; | ||
39 | +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; | ||
40 | +import org.thingsboard.server.dao.entity.EntityService; | ||
41 | +import org.thingsboard.server.dao.entityview.EntityViewService; | ||
42 | +import org.thingsboard.server.dao.timeseries.TimeseriesService; | ||
43 | +import org.thingsboard.server.queue.discovery.ClusterTopologyChangeEvent; | ||
44 | +import org.thingsboard.server.queue.discovery.PartitionChangeEvent; | ||
45 | +import org.thingsboard.server.queue.discovery.PartitionService; | ||
46 | +import org.thingsboard.server.queue.util.TbCoreComponent; | ||
47 | +import org.thingsboard.server.service.queue.TbClusterService; | ||
48 | +import org.thingsboard.server.service.telemetry.TelemetryWebSocketService; | ||
49 | +import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; | ||
50 | +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; | ||
51 | +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd; | ||
52 | +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; | ||
53 | +import org.thingsboard.server.service.telemetry.cmd.v2.EntityHistoryCmd; | ||
54 | +import org.thingsboard.server.service.telemetry.cmd.v2.LatestValueCmd; | ||
55 | +import org.thingsboard.server.service.telemetry.cmd.v2.TimeSeriesCmd; | ||
56 | +import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate; | ||
57 | + | ||
58 | +import javax.annotation.PostConstruct; | ||
59 | +import javax.annotation.PreDestroy; | ||
60 | +import java.util.ArrayList; | ||
61 | +import java.util.HashMap; | ||
62 | +import java.util.LinkedHashMap; | ||
63 | +import java.util.List; | ||
64 | +import java.util.Map; | ||
65 | +import java.util.Set; | ||
66 | +import java.util.concurrent.ConcurrentHashMap; | ||
67 | +import java.util.concurrent.ExecutionException; | ||
68 | +import java.util.concurrent.ExecutorService; | ||
69 | +import java.util.concurrent.Executors; | ||
70 | +import java.util.function.Function; | ||
71 | +import java.util.stream.Collectors; | ||
72 | + | ||
73 | +@Slf4j | ||
74 | +@TbCoreComponent | ||
75 | +@Service | ||
76 | +public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubscriptionService { | ||
77 | + | ||
78 | + private static final int DEFAULT_LIMIT = 100; | ||
79 | + private final Set<TopicPartitionInfo> currentPartitions = ConcurrentHashMap.newKeySet(); | ||
80 | + private final Map<String, Map<Integer, TbSubscription>> subscriptionsBySessionId = new ConcurrentHashMap<>(); | ||
81 | + | ||
82 | + @Autowired | ||
83 | + private TelemetryWebSocketService wsService; | ||
84 | + | ||
85 | + @Autowired | ||
86 | + private EntityViewService entityViewService; | ||
87 | + | ||
88 | + @Autowired | ||
89 | + private EntityService entityService; | ||
90 | + | ||
91 | + @Autowired | ||
92 | + private PartitionService partitionService; | ||
93 | + | ||
94 | + @Autowired | ||
95 | + private TbClusterService clusterService; | ||
96 | + | ||
97 | + @Autowired | ||
98 | + @Lazy | ||
99 | + private SubscriptionManagerService subscriptionManagerService; | ||
100 | + | ||
101 | + @Autowired | ||
102 | + private TimeseriesService tsService; | ||
103 | + | ||
104 | + private ExecutorService wsCallBackExecutor; | ||
105 | + | ||
106 | + @PostConstruct | ||
107 | + public void initExecutor() { | ||
108 | + wsCallBackExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("ws-entity-sub-callback")); | ||
109 | + } | ||
110 | + | ||
111 | + @PreDestroy | ||
112 | + public void shutdownExecutor() { | ||
113 | + if (wsCallBackExecutor != null) { | ||
114 | + wsCallBackExecutor.shutdownNow(); | ||
115 | + } | ||
116 | + } | ||
117 | + | ||
118 | + @Override | ||
119 | + @EventListener(PartitionChangeEvent.class) | ||
120 | + public void onApplicationEvent(PartitionChangeEvent partitionChangeEvent) { | ||
121 | + if (ServiceType.TB_CORE.equals(partitionChangeEvent.getServiceType())) { | ||
122 | + currentPartitions.clear(); | ||
123 | + currentPartitions.addAll(partitionChangeEvent.getPartitions()); | ||
124 | + } | ||
125 | + } | ||
126 | + | ||
127 | + @Override | ||
128 | + @EventListener(ClusterTopologyChangeEvent.class) | ||
129 | + public void onApplicationEvent(ClusterTopologyChangeEvent event) { | ||
130 | + if (event.getServiceQueueKeys().stream().anyMatch(key -> ServiceType.TB_CORE.equals(key.getServiceType()))) { | ||
131 | + /* | ||
132 | + * If the cluster topology has changed, we need to push all current subscriptions to SubscriptionManagerService again. | ||
133 | + * Otherwise, the SubscriptionManagerService may "forget" those subscriptions in case of restart. | ||
134 | + * Although this is resource consuming operation, it is cheaper than sending ping/pong commands periodically | ||
135 | + * It is also cheaper then caching the subscriptions by entity id and then lookup of those caches every time we have new telemetry in SubscriptionManagerService. | ||
136 | + * Even if we cache locally the list of active subscriptions by entity id, it is still time consuming operation to get them from cache | ||
137 | + * Since number of subscriptions is usually much less then number of devices that are pushing data. | ||
138 | +// */ | ||
139 | +// subscriptionsBySessionId.values().forEach(map -> map.values() | ||
140 | +// .forEach(sub -> pushSubscriptionToManagerService(sub, false))); | ||
141 | + } | ||
142 | + } | ||
143 | + | ||
144 | + @Override | ||
145 | + public void handleCmd(TelemetryWebSocketSessionRef session, EntityDataCmd cmd) { | ||
146 | + if (cmd.getHistoryCmd() != null) { | ||
147 | + handleHistoryCmd(session, cmd.getCmdId(), cmd.getQuery(), cmd.getHistoryCmd()); | ||
148 | + } else if (cmd.getLatestCmd() != null) { | ||
149 | + handleLatestCmd(session, cmd.getCmdId(), cmd.getQuery(), cmd.getLatestCmd()); | ||
150 | + } else { | ||
151 | + handleTimeseriesCmd(session, cmd.getCmdId(), cmd.getQuery(), cmd.getTsCmd()); | ||
152 | + } | ||
153 | + } | ||
154 | + | ||
155 | + private void handleTimeseriesCmd(TelemetryWebSocketSessionRef session, int cmdId, EntityDataQuery query, TimeSeriesCmd tsCmd) { | ||
156 | + } | ||
157 | + | ||
158 | + private void handleLatestCmd(TelemetryWebSocketSessionRef session, int cmdId, EntityDataQuery query, LatestValueCmd latestCmd) { | ||
159 | + | ||
160 | + } | ||
161 | + | ||
162 | + private void handleHistoryCmd(TelemetryWebSocketSessionRef session, int cmdId, EntityDataQuery query, EntityHistoryCmd historyCmd) { | ||
163 | + TenantId tenantId = session.getSecurityCtx().getTenantId(); | ||
164 | + CustomerId customerId = session.getSecurityCtx().getCustomerId(); | ||
165 | + PageData<EntityData> data = entityService.findEntityDataByQuery(tenantId, customerId, query); | ||
166 | + List<ReadTsKvQuery> tsKvQueryList = historyCmd.getKeys().stream().map(key -> new BaseReadTsKvQuery( | ||
167 | + key, historyCmd.getStartTs(), historyCmd.getEndTs(), historyCmd.getInterval(), getLimit(historyCmd.getLimit()), historyCmd.getAgg() | ||
168 | + )).collect(Collectors.toList()); | ||
169 | + Map<EntityData, ListenableFuture<List<TsKvEntry>>> fetchResultMap = new HashMap<>(); | ||
170 | + data.getData().forEach(entityData -> fetchResultMap.put(entityData, | ||
171 | + tsService.findAll(tenantId, entityData.getEntityId(), tsKvQueryList))); | ||
172 | + Futures.allAsList(fetchResultMap.values()).addListener(() -> { | ||
173 | + fetchResultMap.forEach((entityData, future) -> { | ||
174 | + Map<String, List<TsValue>> keyData = new LinkedHashMap<>(); | ||
175 | + historyCmd.getKeys().forEach(key -> keyData.put(key, new ArrayList<>())); | ||
176 | + try { | ||
177 | + List<TsKvEntry> entityTsData = future.get(); | ||
178 | + if (entityTsData != null) { | ||
179 | + entityTsData.forEach(entry -> keyData.get(entry.getKey()).add(new TsValue(entry.getTs(), entry.getValueAsString()))); | ||
180 | + } | ||
181 | + keyData.forEach((k, v) -> entityData.getTimeseries().put(k, v.toArray(new TsValue[v.size()]))); | ||
182 | + } catch (InterruptedException | ExecutionException e) { | ||
183 | + log.warn("[{}][{}][{}] Failed to fetch historical data", session.getSessionId(), cmdId, entityData.getEntityId(), e); | ||
184 | + } | ||
185 | + }); | ||
186 | + EntityDataUpdate update = new EntityDataUpdate(cmdId, data, null); | ||
187 | + wsService.sendWsMsg(session.getSessionId(), update); | ||
188 | + }, wsCallBackExecutor); | ||
189 | + } | ||
190 | + | ||
191 | + | ||
192 | + @Override | ||
193 | + public void cancelSubscription(String sessionId, EntityDataUnsubscribeCmd subscriptionId) { | ||
194 | + | ||
195 | + } | ||
196 | + | ||
197 | +// //TODO 3.1: replace null callbacks with callbacks from websocket service. | ||
198 | +// @Override | ||
199 | +// public void addSubscription(TbSubscription subscription) { | ||
200 | +// EntityId entityId = subscription.getEntityId(); | ||
201 | +// // Telemetry subscription on Entity Views are handled differently, because we need to allow only certain keys and time ranges; | ||
202 | +// if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW) && TbSubscriptionType.TIMESERIES.equals(subscription.getType())) { | ||
203 | +// subscription = resolveEntityViewSubscription((TbTimeseriesSubscription) subscription); | ||
204 | +// } | ||
205 | +// pushSubscriptionToManagerService(subscription, true); | ||
206 | +// registerSubscription(subscription); | ||
207 | +// } | ||
208 | + | ||
209 | +// private void pushSubscriptionToManagerService(TbSubscription subscription, boolean pushToLocalService) { | ||
210 | +// TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, subscription.getTenantId(), subscription.getEntityId()); | ||
211 | +// if (currentPartitions.contains(tpi)) { | ||
212 | +// // Subscription is managed on the same server; | ||
213 | +// if (pushToLocalService) { | ||
214 | +// subscriptionManagerService.addSubscription(subscription, TbCallback.EMPTY); | ||
215 | +// } | ||
216 | +// } else { | ||
217 | +// // Push to the queue; | ||
218 | +// TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toNewSubscriptionProto(subscription); | ||
219 | +// clusterService.pushMsgToCore(tpi, subscription.getEntityId().getId(), toCoreMsg, null); | ||
220 | +// } | ||
221 | +// } | ||
222 | + | ||
223 | + @Override | ||
224 | + public void onSubscriptionUpdate(String sessionId, SubscriptionUpdate update, TbCallback callback) { | ||
225 | +// TbSubscription subscription = subscriptionsBySessionId | ||
226 | +// .getOrDefault(sessionId, Collections.emptyMap()).get(update.getSubscriptionId()); | ||
227 | +// if (subscription != null) { | ||
228 | +// switch (subscription.getType()) { | ||
229 | +// case TIMESERIES: | ||
230 | +// TbTimeseriesSubscription tsSub = (TbTimeseriesSubscription) subscription; | ||
231 | +// update.getLatestValues().forEach((key, value) -> tsSub.getKeyStates().put(key, value)); | ||
232 | +// break; | ||
233 | +// case ATTRIBUTES: | ||
234 | +// TbAttributeSubscription attrSub = (TbAttributeSubscription) subscription; | ||
235 | +// update.getLatestValues().forEach((key, value) -> attrSub.getKeyStates().put(key, value)); | ||
236 | +// break; | ||
237 | +// } | ||
238 | +// wsService.sendWsMsg(sessionId, update); | ||
239 | +// } | ||
240 | +// callback.onSuccess(); | ||
241 | + } | ||
242 | + | ||
243 | +// @Override | ||
244 | +// public void cancelSubscription(String sessionId, int subscriptionId) { | ||
245 | +// log.debug("[{}][{}] Going to remove subscription.", sessionId, subscriptionId); | ||
246 | +// Map<Integer, TbSubscription> sessionSubscriptions = subscriptionsBySessionId.get(sessionId); | ||
247 | +// if (sessionSubscriptions != null) { | ||
248 | +// TbSubscription subscription = sessionSubscriptions.remove(subscriptionId); | ||
249 | +// if (subscription != null) { | ||
250 | +// if (sessionSubscriptions.isEmpty()) { | ||
251 | +// subscriptionsBySessionId.remove(sessionId); | ||
252 | +// } | ||
253 | +// TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, subscription.getTenantId(), subscription.getEntityId()); | ||
254 | +// if (currentPartitions.contains(tpi)) { | ||
255 | +// // Subscription is managed on the same server; | ||
256 | +// subscriptionManagerService.cancelSubscription(sessionId, subscriptionId, TbCallback.EMPTY); | ||
257 | +// } else { | ||
258 | +// // Push to the queue; | ||
259 | +// TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toCloseSubscriptionProto(subscription); | ||
260 | +// clusterService.pushMsgToCore(tpi, subscription.getEntityId().getId(), toCoreMsg, null); | ||
261 | +// } | ||
262 | +// } else { | ||
263 | +// log.debug("[{}][{}] Subscription not found!", sessionId, subscriptionId); | ||
264 | +// } | ||
265 | +// } else { | ||
266 | +// log.debug("[{}] No session subscriptions found!", sessionId); | ||
267 | +// } | ||
268 | +// } | ||
269 | + | ||
270 | + @Override | ||
271 | + public void cancelAllSessionSubscriptions(String sessionId) { | ||
272 | +// Map<Integer, TbSubscription> subscriptions = subscriptionsBySessionId.get(sessionId); | ||
273 | +// if (subscriptions != null) { | ||
274 | +// Set<Integer> toRemove = new HashSet<>(subscriptions.keySet()); | ||
275 | +// toRemove.forEach(id -> cancelSubscription(sessionId, id)); | ||
276 | +// } | ||
277 | + } | ||
278 | + | ||
279 | + private TbSubscription resolveEntityViewSubscription(TbTimeseriesSubscription subscription) { | ||
280 | + EntityView entityView = entityViewService.findEntityViewById(TenantId.SYS_TENANT_ID, new EntityViewId(subscription.getEntityId().getId())); | ||
281 | + | ||
282 | + Map<String, Long> keyStates; | ||
283 | + if (subscription.isAllKeys()) { | ||
284 | + keyStates = entityView.getKeys().getTimeseries().stream().collect(Collectors.toMap(k -> k, k -> 0L)); | ||
285 | + } else { | ||
286 | + keyStates = subscription.getKeyStates().entrySet() | ||
287 | + .stream().filter(entry -> entityView.getKeys().getTimeseries().contains(entry.getKey())) | ||
288 | + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); | ||
289 | + } | ||
290 | + | ||
291 | + return TbTimeseriesSubscription.builder() | ||
292 | + .serviceId(subscription.getServiceId()) | ||
293 | + .sessionId(subscription.getSessionId()) | ||
294 | + .subscriptionId(subscription.getSubscriptionId()) | ||
295 | + .tenantId(subscription.getTenantId()) | ||
296 | + .entityId(entityView.getEntityId()) | ||
297 | + .startTime(entityView.getStartTimeMs()) | ||
298 | + .endTime(entityView.getEndTimeMs()) | ||
299 | + .allKeys(false) | ||
300 | + .keyStates(keyStates).build(); | ||
301 | + } | ||
302 | + | ||
303 | + private void registerSubscription(TbSubscription subscription) { | ||
304 | + Map<Integer, TbSubscription> sessionSubscriptions = subscriptionsBySessionId.computeIfAbsent(subscription.getSessionId(), k -> new ConcurrentHashMap<>()); | ||
305 | + sessionSubscriptions.put(subscription.getSubscriptionId(), subscription); | ||
306 | + } | ||
307 | + | ||
308 | + private int getLimit(int limit) { | ||
309 | + return limit == 0 ? DEFAULT_LIMIT : limit; | ||
310 | + } | ||
311 | + | ||
312 | +} |
1 | +/** | ||
2 | + * Copyright © 2016-2020 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.service.subscription; | ||
17 | + | ||
18 | +import org.thingsboard.server.common.msg.queue.TbCallback; | ||
19 | +import org.thingsboard.server.queue.discovery.ClusterTopologyChangeEvent; | ||
20 | +import org.thingsboard.server.queue.discovery.PartitionChangeEvent; | ||
21 | +import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; | ||
22 | +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; | ||
23 | +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd; | ||
24 | +import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate; | ||
25 | + | ||
26 | +public interface TbEntityDataSubscriptionService { | ||
27 | + | ||
28 | + void handleCmd(TelemetryWebSocketSessionRef sessionId, EntityDataCmd cmd); | ||
29 | + | ||
30 | + void cancelSubscription(String sessionId, EntityDataUnsubscribeCmd subscriptionId); | ||
31 | + | ||
32 | + void cancelAllSessionSubscriptions(String sessionId); | ||
33 | + | ||
34 | + void onSubscriptionUpdate(String sessionId, SubscriptionUpdate update, TbCallback callback); | ||
35 | + | ||
36 | + void onApplicationEvent(PartitionChangeEvent event); | ||
37 | + | ||
38 | + void onApplicationEvent(ClusterTopologyChangeEvent event); | ||
39 | +} |
@@ -49,8 +49,10 @@ import org.thingsboard.server.service.security.AccessValidator; | @@ -49,8 +49,10 @@ import org.thingsboard.server.service.security.AccessValidator; | ||
49 | import org.thingsboard.server.service.security.ValidationCallback; | 49 | import org.thingsboard.server.service.security.ValidationCallback; |
50 | import org.thingsboard.server.service.security.ValidationResult; | 50 | import org.thingsboard.server.service.security.ValidationResult; |
51 | import org.thingsboard.server.service.security.ValidationResultCode; | 51 | import org.thingsboard.server.service.security.ValidationResultCode; |
52 | +import org.thingsboard.server.service.security.model.SecurityUser; | ||
52 | import org.thingsboard.server.service.security.model.UserPrincipal; | 53 | import org.thingsboard.server.service.security.model.UserPrincipal; |
53 | import org.thingsboard.server.service.security.permission.Operation; | 54 | import org.thingsboard.server.service.security.permission.Operation; |
55 | +import org.thingsboard.server.service.subscription.TbEntityDataSubscriptionService; | ||
54 | import org.thingsboard.server.service.subscription.TbLocalSubscriptionService; | 56 | import org.thingsboard.server.service.subscription.TbLocalSubscriptionService; |
55 | import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope; | 57 | import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope; |
56 | import org.thingsboard.server.service.subscription.TbAttributeSubscription; | 58 | import org.thingsboard.server.service.subscription.TbAttributeSubscription; |
@@ -61,6 +63,9 @@ import org.thingsboard.server.service.telemetry.cmd.v1.SubscriptionCmd; | @@ -61,6 +63,9 @@ import org.thingsboard.server.service.telemetry.cmd.v1.SubscriptionCmd; | ||
61 | import org.thingsboard.server.service.telemetry.cmd.v1.TelemetryPluginCmd; | 63 | import org.thingsboard.server.service.telemetry.cmd.v1.TelemetryPluginCmd; |
62 | import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper; | 64 | import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper; |
63 | import org.thingsboard.server.service.telemetry.cmd.v1.TimeseriesSubscriptionCmd; | 65 | import org.thingsboard.server.service.telemetry.cmd.v1.TimeseriesSubscriptionCmd; |
66 | +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; | ||
67 | +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd; | ||
68 | +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; | ||
64 | import org.thingsboard.server.service.telemetry.exception.UnauthorizedException; | 69 | import org.thingsboard.server.service.telemetry.exception.UnauthorizedException; |
65 | import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; | 70 | import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; |
66 | import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate; | 71 | import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate; |
@@ -104,7 +109,10 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi | @@ -104,7 +109,10 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi | ||
104 | private final ConcurrentMap<String, WsSessionMetaData> wsSessionsMap = new ConcurrentHashMap<>(); | 109 | private final ConcurrentMap<String, WsSessionMetaData> wsSessionsMap = new ConcurrentHashMap<>(); |
105 | 110 | ||
106 | @Autowired | 111 | @Autowired |
107 | - private TbLocalSubscriptionService subService; | 112 | + private TbLocalSubscriptionService oldSubService; |
113 | + | ||
114 | + @Autowired | ||
115 | + private TbEntityDataSubscriptionService entityDataSubService; | ||
108 | 116 | ||
109 | @Autowired | 117 | @Autowired |
110 | private TelemetryWebSocketMsgEndpoint msgEndpoint; | 118 | private TelemetryWebSocketMsgEndpoint msgEndpoint; |
@@ -164,7 +172,8 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi | @@ -164,7 +172,8 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi | ||
164 | break; | 172 | break; |
165 | case CLOSED: | 173 | case CLOSED: |
166 | wsSessionsMap.remove(sessionId); | 174 | wsSessionsMap.remove(sessionId); |
167 | - subService.cancelAllSessionSubscriptions(sessionId); | 175 | + oldSubService.cancelAllSessionSubscriptions(sessionId); |
176 | + entityDataSubService.cancelAllSessionSubscriptions(sessionId); | ||
168 | processSessionClose(sessionRef); | 177 | processSessionClose(sessionRef); |
169 | break; | 178 | break; |
170 | } | 179 | } |
@@ -196,6 +205,12 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi | @@ -196,6 +205,12 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi | ||
196 | if (cmdsWrapper.getHistoryCmds() != null) { | 205 | if (cmdsWrapper.getHistoryCmds() != null) { |
197 | cmdsWrapper.getHistoryCmds().forEach(cmd -> handleWsHistoryCmd(sessionRef, cmd)); | 206 | cmdsWrapper.getHistoryCmds().forEach(cmd -> handleWsHistoryCmd(sessionRef, cmd)); |
198 | } | 207 | } |
208 | + if (cmdsWrapper.getEntityDataCmds() != null) { | ||
209 | + cmdsWrapper.getEntityDataCmds().forEach(cmd -> handleWsEntityDataCmd(sessionRef, cmd)); | ||
210 | + } | ||
211 | + if (cmdsWrapper.getEntityDataUnsubscribeCmds() != null) { | ||
212 | + cmdsWrapper.getEntityDataUnsubscribeCmds().forEach(cmd -> handleWsEntityDataUnsubscribeCmd(sessionRef, cmd)); | ||
213 | + } | ||
199 | } | 214 | } |
200 | } catch (IOException e) { | 215 | } catch (IOException e) { |
201 | log.warn("Failed to decode subscription cmd: {}", e.getMessage(), e); | 216 | log.warn("Failed to decode subscription cmd: {}", e.getMessage(), e); |
@@ -204,11 +219,39 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi | @@ -204,11 +219,39 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi | ||
204 | } | 219 | } |
205 | } | 220 | } |
206 | 221 | ||
222 | + private void handleWsEntityDataCmd(TelemetryWebSocketSessionRef sessionRef, EntityDataCmd cmd) { | ||
223 | + String sessionId = sessionRef.getSessionId(); | ||
224 | + log.debug("[{}] Processing: {}", sessionId, cmd); | ||
225 | + | ||
226 | + if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId) | ||
227 | + && validateSubscriptionCmd(sessionRef, cmd)) { | ||
228 | + entityDataSubService.handleCmd(sessionRef, cmd); | ||
229 | + } | ||
230 | + } | ||
231 | + | ||
232 | + private void handleWsEntityDataUnsubscribeCmd(TelemetryWebSocketSessionRef sessionRef, EntityDataUnsubscribeCmd cmd) { | ||
233 | + String sessionId = sessionRef.getSessionId(); | ||
234 | + log.debug("[{}] Processing: {}", sessionId, cmd); | ||
235 | + | ||
236 | + if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId)) { | ||
237 | + entityDataSubService.cancelSubscription(sessionRef.getSessionId(), cmd); | ||
238 | + } | ||
239 | + } | ||
240 | + | ||
207 | @Override | 241 | @Override |
208 | public void sendWsMsg(String sessionId, SubscriptionUpdate update) { | 242 | public void sendWsMsg(String sessionId, SubscriptionUpdate update) { |
243 | + sendWsMsg(sessionId, update.getSubscriptionId(), update); | ||
244 | + } | ||
245 | + | ||
246 | + @Override | ||
247 | + public void sendWsMsg(String sessionId, EntityDataUpdate update) { | ||
248 | + sendWsMsg(sessionId, update.getCmdId(), update); | ||
249 | + } | ||
250 | + | ||
251 | + private <T> void sendWsMsg(String sessionId, int cmdId, T update) { | ||
209 | WsSessionMetaData md = wsSessionsMap.get(sessionId); | 252 | WsSessionMetaData md = wsSessionsMap.get(sessionId); |
210 | if (md != null) { | 253 | if (md != null) { |
211 | - sendWsMsg(md.getSessionRef(), update); | 254 | + sendWsMsg(md.getSessionRef(), cmdId, update); |
212 | } | 255 | } |
213 | } | 256 | } |
214 | 257 | ||
@@ -356,7 +399,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi | @@ -356,7 +399,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi | ||
356 | .allKeys(false) | 399 | .allKeys(false) |
357 | .keyStates(subState) | 400 | .keyStates(subState) |
358 | .scope(scope).build(); | 401 | .scope(scope).build(); |
359 | - subService.addSubscription(sub); | 402 | + oldSubService.addSubscription(sub); |
360 | } | 403 | } |
361 | 404 | ||
362 | @Override | 405 | @Override |
@@ -453,7 +496,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi | @@ -453,7 +496,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi | ||
453 | .allKeys(true) | 496 | .allKeys(true) |
454 | .keyStates(subState) | 497 | .keyStates(subState) |
455 | .scope(scope).build(); | 498 | .scope(scope).build(); |
456 | - subService.addSubscription(sub); | 499 | + oldSubService.addSubscription(sub); |
457 | } | 500 | } |
458 | 501 | ||
459 | @Override | 502 | @Override |
@@ -534,7 +577,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi | @@ -534,7 +577,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi | ||
534 | .entityId(entityId) | 577 | .entityId(entityId) |
535 | .allKeys(true) | 578 | .allKeys(true) |
536 | .keyStates(subState).build(); | 579 | .keyStates(subState).build(); |
537 | - subService.addSubscription(sub); | 580 | + oldSubService.addSubscription(sub); |
538 | } | 581 | } |
539 | 582 | ||
540 | @Override | 583 | @Override |
@@ -571,7 +614,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi | @@ -571,7 +614,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi | ||
571 | .entityId(entityId) | 614 | .entityId(entityId) |
572 | .allKeys(false) | 615 | .allKeys(false) |
573 | .keyStates(subState).build(); | 616 | .keyStates(subState).build(); |
574 | - subService.addSubscription(sub); | 617 | + oldSubService.addSubscription(sub); |
575 | } | 618 | } |
576 | 619 | ||
577 | @Override | 620 | @Override |
@@ -590,12 +633,32 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi | @@ -590,12 +633,32 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi | ||
590 | 633 | ||
591 | private void unsubscribe(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) { | 634 | private void unsubscribe(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) { |
592 | if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) { | 635 | if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) { |
593 | - subService.cancelAllSessionSubscriptions(sessionId); | 636 | + oldSubService.cancelAllSessionSubscriptions(sessionId); |
594 | } else { | 637 | } else { |
595 | - subService.cancelSubscription(sessionId, cmd.getCmdId()); | 638 | + oldSubService.cancelSubscription(sessionId, cmd.getCmdId()); |
596 | } | 639 | } |
597 | } | 640 | } |
598 | 641 | ||
642 | + private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, EntityDataCmd cmd) { | ||
643 | + if (cmd.getCmdId() < 0) { | ||
644 | + SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, | ||
645 | + "Cmd id is negative value!"); | ||
646 | + sendWsMsg(sessionRef, update); | ||
647 | + return false; | ||
648 | + } else if (cmd.getQuery() == null) { | ||
649 | + SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, | ||
650 | + "Query is empty!"); | ||
651 | + sendWsMsg(sessionRef, update); | ||
652 | + return false; | ||
653 | + } else if (cmd.getHistoryCmd() == null && cmd.getLatestCmd() == null && cmd.getTsCmd() == null) { | ||
654 | + SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, | ||
655 | + "No history, latest or timeseries command present!"); | ||
656 | + sendWsMsg(sessionRef, update); | ||
657 | + return false; | ||
658 | + } | ||
659 | + return true; | ||
660 | + } | ||
661 | + | ||
599 | private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd) { | 662 | private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd) { |
600 | if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) { | 663 | if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) { |
601 | SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, | 664 | SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, |
@@ -607,10 +670,14 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi | @@ -607,10 +670,14 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi | ||
607 | } | 670 | } |
608 | 671 | ||
609 | private boolean validateSessionMetadata(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) { | 672 | private boolean validateSessionMetadata(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) { |
673 | + return validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId); | ||
674 | + } | ||
675 | + | ||
676 | + private boolean validateSessionMetadata(TelemetryWebSocketSessionRef sessionRef, int cmdId, String sessionId) { | ||
610 | WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId); | 677 | WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId); |
611 | if (sessionMD == null) { | 678 | if (sessionMD == null) { |
612 | log.warn("[{}] Session meta data not found. ", sessionId); | 679 | log.warn("[{}] Session meta data not found. ", sessionId); |
613 | - SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR, | 680 | + SubscriptionUpdate update = new SubscriptionUpdate(cmdId, SubscriptionErrorCode.INTERNAL_ERROR, |
614 | SESSION_META_DATA_NOT_FOUND); | 681 | SESSION_META_DATA_NOT_FOUND); |
615 | sendWsMsg(sessionRef, update); | 682 | sendWsMsg(sessionRef, update); |
616 | return false; | 683 | return false; |
@@ -619,10 +686,18 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi | @@ -619,10 +686,18 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi | ||
619 | } | 686 | } |
620 | } | 687 | } |
621 | 688 | ||
689 | + private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, EntityDataUpdate update) { | ||
690 | + sendWsMsg(sessionRef, update.getCmdId(), update); | ||
691 | + } | ||
692 | + | ||
622 | private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, SubscriptionUpdate update) { | 693 | private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, SubscriptionUpdate update) { |
694 | + sendWsMsg(sessionRef, update.getSubscriptionId(), update); | ||
695 | + } | ||
696 | + | ||
697 | + private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, int cmdId, Object update) { | ||
623 | executor.submit(() -> { | 698 | executor.submit(() -> { |
624 | try { | 699 | try { |
625 | - msgEndpoint.send(sessionRef, update.getSubscriptionId(), jsonMapper.writeValueAsString(update)); | 700 | + msgEndpoint.send(sessionRef, cmdId, jsonMapper.writeValueAsString(update)); |
626 | } catch (JsonProcessingException e) { | 701 | } catch (JsonProcessingException e) { |
627 | log.warn("[{}] Failed to encode reply: {}", sessionRef.getSessionId(), update, e); | 702 | log.warn("[{}] Failed to encode reply: {}", sessionRef.getSessionId(), update, e); |
628 | } catch (IOException e) { | 703 | } catch (IOException e) { |
@@ -631,6 +706,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi | @@ -631,6 +706,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi | ||
631 | }); | 706 | }); |
632 | } | 707 | } |
633 | 708 | ||
709 | + | ||
634 | private static Optional<Set<String>> getKeys(TelemetryPluginCmd cmd) { | 710 | private static Optional<Set<String>> getKeys(TelemetryPluginCmd cmd) { |
635 | if (!StringUtils.isEmpty(cmd.getKeys())) { | 711 | if (!StringUtils.isEmpty(cmd.getKeys())) { |
636 | Set<String> keys = new HashSet<>(); | 712 | Set<String> keys = new HashSet<>(); |
@@ -15,6 +15,7 @@ | @@ -15,6 +15,7 @@ | ||
15 | */ | 15 | */ |
16 | package org.thingsboard.server.service.telemetry; | 16 | package org.thingsboard.server.service.telemetry; |
17 | 17 | ||
18 | +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; | ||
18 | import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate; | 19 | import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate; |
19 | 20 | ||
20 | /** | 21 | /** |
@@ -27,4 +28,7 @@ public interface TelemetryWebSocketService { | @@ -27,4 +28,7 @@ public interface TelemetryWebSocketService { | ||
27 | void handleWebSocketMsg(TelemetryWebSocketSessionRef sessionRef, String msg); | 28 | void handleWebSocketMsg(TelemetryWebSocketSessionRef sessionRef, String msg); |
28 | 29 | ||
29 | void sendWsMsg(String sessionId, SubscriptionUpdate update); | 30 | void sendWsMsg(String sessionId, SubscriptionUpdate update); |
31 | + | ||
32 | + void sendWsMsg(String sessionId, EntityDataUpdate update); | ||
33 | + | ||
30 | } | 34 | } |
@@ -15,10 +15,12 @@ | @@ -15,10 +15,12 @@ | ||
15 | */ | 15 | */ |
16 | package org.thingsboard.server.service.telemetry.cmd.v2; | 16 | package org.thingsboard.server.service.telemetry.cmd.v2; |
17 | 17 | ||
18 | +import lombok.Data; | ||
18 | import org.thingsboard.server.common.data.kv.Aggregation; | 19 | import org.thingsboard.server.common.data.kv.Aggregation; |
19 | 20 | ||
20 | import java.util.List; | 21 | import java.util.List; |
21 | 22 | ||
23 | +@Data | ||
22 | public class EntityHistoryCmd { | 24 | public class EntityHistoryCmd { |
23 | 25 | ||
24 | private List<String> keys; | 26 | private List<String> keys; |
@@ -91,409 +91,8 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppC | @@ -91,409 +91,8 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppC | ||
91 | @Configuration | 91 | @Configuration |
92 | @ComponentScan({"org.thingsboard.server"}) | 92 | @ComponentScan({"org.thingsboard.server"}) |
93 | @WebAppConfiguration | 93 | @WebAppConfiguration |
94 | -@SpringBootTest | 94 | +@SpringBootTest() |
95 | @Slf4j | 95 | @Slf4j |
96 | -public abstract class AbstractControllerTest { | ||
97 | - | ||
98 | - protected ObjectMapper mapper = new ObjectMapper(); | ||
99 | - | ||
100 | - protected static final String TEST_TENANT_NAME = "TEST TENANT"; | ||
101 | - | ||
102 | - protected static final String SYS_ADMIN_EMAIL = "sysadmin@thingsboard.org"; | ||
103 | - private static final String SYS_ADMIN_PASSWORD = "sysadmin"; | ||
104 | - | ||
105 | - protected static final String TENANT_ADMIN_EMAIL = "testtenant@thingsboard.org"; | ||
106 | - private static final String TENANT_ADMIN_PASSWORD = "tenant"; | ||
107 | - | ||
108 | - protected static final String CUSTOMER_USER_EMAIL = "testcustomer@thingsboard.org"; | ||
109 | - private static final String CUSTOMER_USER_PASSWORD = "customer"; | ||
110 | - | ||
111 | - /** See {@link org.springframework.test.web.servlet.DefaultMvcResult#getAsyncResult(long)} | ||
112 | - * and {@link org.springframework.mock.web.MockAsyncContext#getTimeout()} | ||
113 | - */ | ||
114 | - private static final long DEFAULT_TIMEOUT = -1L; | ||
115 | - | ||
116 | - protected MediaType contentType = MediaType.APPLICATION_JSON; | ||
117 | - | ||
118 | - protected MockMvc mockMvc; | ||
119 | - | ||
120 | - protected String token; | ||
121 | - protected String refreshToken; | ||
122 | - protected String username; | ||
123 | - | ||
124 | - private TenantId tenantId; | ||
125 | - | ||
126 | - @SuppressWarnings("rawtypes") | ||
127 | - private HttpMessageConverter mappingJackson2HttpMessageConverter; | ||
128 | - | ||
129 | - @SuppressWarnings("rawtypes") | ||
130 | - private HttpMessageConverter stringHttpMessageConverter; | ||
131 | - | ||
132 | - @Autowired | ||
133 | - private WebApplicationContext webApplicationContext; | ||
134 | - | ||
135 | - @Rule | ||
136 | - public TestRule watcher = new TestWatcher() { | ||
137 | - protected void starting(Description description) { | ||
138 | - log.info("Starting test: {}", description.getMethodName()); | ||
139 | - } | ||
140 | - | ||
141 | - protected void finished(Description description) { | ||
142 | - log.info("Finished test: {}", description.getMethodName()); | ||
143 | - } | ||
144 | - }; | ||
145 | - | ||
146 | - @Autowired | ||
147 | - void setConverters(HttpMessageConverter<?>[] converters) { | ||
148 | - | ||
149 | - this.mappingJackson2HttpMessageConverter = Arrays.stream(converters) | ||
150 | - .filter(hmc -> hmc instanceof MappingJackson2HttpMessageConverter) | ||
151 | - .findAny() | ||
152 | - .get(); | ||
153 | - | ||
154 | - this.stringHttpMessageConverter = Arrays.stream(converters) | ||
155 | - .filter(hmc -> hmc instanceof StringHttpMessageConverter) | ||
156 | - .findAny() | ||
157 | - .get(); | ||
158 | - | ||
159 | - Assert.assertNotNull("the JSON message converter must not be null", | ||
160 | - this.mappingJackson2HttpMessageConverter); | ||
161 | - } | ||
162 | - | ||
163 | - @Before | ||
164 | - public void setup() throws Exception { | ||
165 | - log.info("Executing setup"); | ||
166 | - if (this.mockMvc == null) { | ||
167 | - this.mockMvc = webAppContextSetup(webApplicationContext) | ||
168 | - .apply(springSecurity()).build(); | ||
169 | - } | ||
170 | - loginSysAdmin(); | ||
171 | - | ||
172 | - Tenant tenant = new Tenant(); | ||
173 | - tenant.setTitle(TEST_TENANT_NAME); | ||
174 | - Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class); | ||
175 | - Assert.assertNotNull(savedTenant); | ||
176 | - tenantId = savedTenant.getId(); | ||
177 | - | ||
178 | - User tenantAdmin = new User(); | ||
179 | - tenantAdmin.setAuthority(Authority.TENANT_ADMIN); | ||
180 | - tenantAdmin.setTenantId(tenantId); | ||
181 | - tenantAdmin.setEmail(TENANT_ADMIN_EMAIL); | ||
182 | - | ||
183 | - createUserAndLogin(tenantAdmin, TENANT_ADMIN_PASSWORD); | ||
184 | - | ||
185 | - Customer customer = new Customer(); | ||
186 | - customer.setTitle("Customer"); | ||
187 | - customer.setTenantId(tenantId); | ||
188 | - Customer savedCustomer = doPost("/api/customer", customer, Customer.class); | ||
189 | - | ||
190 | - User customerUser = new User(); | ||
191 | - customerUser.setAuthority(Authority.CUSTOMER_USER); | ||
192 | - customerUser.setTenantId(tenantId); | ||
193 | - customerUser.setCustomerId(savedCustomer.getId()); | ||
194 | - customerUser.setEmail(CUSTOMER_USER_EMAIL); | ||
195 | - | ||
196 | - createUserAndLogin(customerUser, CUSTOMER_USER_PASSWORD); | ||
197 | - | ||
198 | - logout(); | ||
199 | - | ||
200 | - log.info("Executed setup"); | ||
201 | - } | ||
202 | - | ||
203 | - @After | ||
204 | - public void teardown() throws Exception { | ||
205 | - log.info("Executing teardown"); | ||
206 | - loginSysAdmin(); | ||
207 | - doDelete("/api/tenant/" + tenantId.getId().toString()) | ||
208 | - .andExpect(status().isOk()); | ||
209 | - log.info("Executed teardown"); | ||
210 | - } | ||
211 | - | ||
212 | - protected void loginSysAdmin() throws Exception { | ||
213 | - login(SYS_ADMIN_EMAIL, SYS_ADMIN_PASSWORD); | ||
214 | - } | ||
215 | - | ||
216 | - protected void loginTenantAdmin() throws Exception { | ||
217 | - login(TENANT_ADMIN_EMAIL, TENANT_ADMIN_PASSWORD); | ||
218 | - } | ||
219 | - | ||
220 | - protected void loginCustomerUser() throws Exception { | ||
221 | - login(CUSTOMER_USER_EMAIL, CUSTOMER_USER_PASSWORD); | ||
222 | - } | ||
223 | - | ||
224 | - private Tenant savedDifferentTenant; | ||
225 | - protected void loginDifferentTenant() throws Exception { | ||
226 | - loginSysAdmin(); | ||
227 | - Tenant tenant = new Tenant(); | ||
228 | - tenant.setTitle("Different tenant"); | ||
229 | - savedDifferentTenant = doPost("/api/tenant", tenant, Tenant.class); | ||
230 | - Assert.assertNotNull(savedDifferentTenant); | ||
231 | - User differentTenantAdmin = new User(); | ||
232 | - differentTenantAdmin.setAuthority(Authority.TENANT_ADMIN); | ||
233 | - differentTenantAdmin.setTenantId(savedDifferentTenant.getId()); | ||
234 | - differentTenantAdmin.setEmail("different_tenant@thingsboard.org"); | ||
235 | - | ||
236 | - createUserAndLogin(differentTenantAdmin, "testPassword"); | ||
237 | - } | ||
238 | - | ||
239 | - protected void deleteDifferentTenant() throws Exception { | ||
240 | - loginSysAdmin(); | ||
241 | - doDelete("/api/tenant/" + savedDifferentTenant.getId().getId().toString()) | ||
242 | - .andExpect(status().isOk()); | ||
243 | - } | ||
244 | - | ||
245 | - protected User createUserAndLogin(User user, String password) throws Exception { | ||
246 | - User savedUser = doPost("/api/user", user, User.class); | ||
247 | - logout(); | ||
248 | - doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken) | ||
249 | - .andExpect(status().isSeeOther()) | ||
250 | - .andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken)); | ||
251 | - JsonNode activateRequest = new ObjectMapper().createObjectNode() | ||
252 | - .put("activateToken", TestMailService.currentActivateToken) | ||
253 | - .put("password", password); | ||
254 | - JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class); | ||
255 | - validateAndSetJwtToken(tokenInfo, user.getEmail()); | ||
256 | - return savedUser; | ||
257 | - } | ||
258 | - | ||
259 | - protected void login(String username, String password) throws Exception { | ||
260 | - this.token = null; | ||
261 | - this.refreshToken = null; | ||
262 | - this.username = null; | ||
263 | - JsonNode tokenInfo = readResponse(doPost("/api/auth/login", new LoginRequest(username, password)).andExpect(status().isOk()), JsonNode.class); | ||
264 | - validateAndSetJwtToken(tokenInfo, username); | ||
265 | - } | ||
266 | - | ||
267 | - protected void refreshToken() throws Exception { | ||
268 | - this.token = null; | ||
269 | - JsonNode tokenInfo = readResponse(doPost("/api/auth/token", new RefreshTokenRequest(this.refreshToken)).andExpect(status().isOk()), JsonNode.class); | ||
270 | - validateAndSetJwtToken(tokenInfo, this.username); | ||
271 | - } | ||
272 | - | ||
273 | - protected void validateAndSetJwtToken(JsonNode tokenInfo, String username) { | ||
274 | - Assert.assertNotNull(tokenInfo); | ||
275 | - Assert.assertTrue(tokenInfo.has("token")); | ||
276 | - Assert.assertTrue(tokenInfo.has("refreshToken")); | ||
277 | - String token = tokenInfo.get("token").asText(); | ||
278 | - String refreshToken = tokenInfo.get("refreshToken").asText(); | ||
279 | - validateJwtToken(token, username); | ||
280 | - validateJwtToken(refreshToken, username); | ||
281 | - this.token = token; | ||
282 | - this.refreshToken = refreshToken; | ||
283 | - this.username = username; | ||
284 | - } | ||
285 | - | ||
286 | - protected void validateJwtToken(String token, String username) { | ||
287 | - Assert.assertNotNull(token); | ||
288 | - Assert.assertFalse(token.isEmpty()); | ||
289 | - int i = token.lastIndexOf('.'); | ||
290 | - Assert.assertTrue(i > 0); | ||
291 | - String withoutSignature = token.substring(0, i + 1); | ||
292 | - Jwt<Header, Claims> jwsClaims = Jwts.parser().parseClaimsJwt(withoutSignature); | ||
293 | - Claims claims = jwsClaims.getBody(); | ||
294 | - String subject = claims.getSubject(); | ||
295 | - Assert.assertEquals(username, subject); | ||
296 | - } | ||
297 | - | ||
298 | - protected void logout() throws Exception { | ||
299 | - this.token = null; | ||
300 | - this.refreshToken = null; | ||
301 | - this.username = null; | ||
302 | - } | ||
303 | - | ||
304 | - protected void setJwtToken(MockHttpServletRequestBuilder request) { | ||
305 | - if (this.token != null) { | ||
306 | - request.header(ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM, "Bearer " + this.token); | ||
307 | - } | ||
308 | - } | ||
309 | - | ||
310 | - protected ResultActions doGet(String urlTemplate, Object... urlVariables) throws Exception { | ||
311 | - MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables); | ||
312 | - setJwtToken(getRequest); | ||
313 | - return mockMvc.perform(getRequest); | ||
314 | - } | ||
315 | - | ||
316 | - protected <T> T doGet(String urlTemplate, Class<T> responseClass, Object... urlVariables) throws Exception { | ||
317 | - return readResponse(doGet(urlTemplate, urlVariables).andExpect(status().isOk()), responseClass); | ||
318 | - } | ||
319 | - | ||
320 | - protected <T> T doGetAsync(String urlTemplate, Class<T> responseClass, Object... urlVariables) throws Exception { | ||
321 | - return readResponse(doGetAsync(urlTemplate, urlVariables).andExpect(status().isOk()), responseClass); | ||
322 | - } | ||
323 | - | ||
324 | - protected ResultActions doGetAsync(String urlTemplate, Object... urlVariables) throws Exception { | ||
325 | - MockHttpServletRequestBuilder getRequest; | ||
326 | - getRequest = get(urlTemplate, urlVariables); | ||
327 | - setJwtToken(getRequest); | ||
328 | - return mockMvc.perform(asyncDispatch(mockMvc.perform(getRequest).andExpect(request().asyncStarted()).andReturn())); | ||
329 | - } | ||
330 | - | ||
331 | - protected <T> T doGetTyped(String urlTemplate, TypeReference<T> responseType, Object... urlVariables) throws Exception { | ||
332 | - return readResponse(doGet(urlTemplate, urlVariables).andExpect(status().isOk()), responseType); | ||
333 | - } | ||
334 | - | ||
335 | - protected <T> T doGetTypedWithPageLink(String urlTemplate, TypeReference<T> responseType, | ||
336 | - PageLink pageLink, | ||
337 | - Object... urlVariables) throws Exception { | ||
338 | - List<Object> pageLinkVariables = new ArrayList<>(); | ||
339 | - urlTemplate += "pageSize={pageSize}&page={page}"; | ||
340 | - pageLinkVariables.add(pageLink.getPageSize()); | ||
341 | - pageLinkVariables.add(pageLink.getPage()); | ||
342 | - if (StringUtils.isNotEmpty(pageLink.getTextSearch())) { | ||
343 | - urlTemplate += "&textSearch={textSearch}"; | ||
344 | - pageLinkVariables.add(pageLink.getTextSearch()); | ||
345 | - } | ||
346 | - if (pageLink.getSortOrder() != null) { | ||
347 | - urlTemplate += "&sortProperty={sortProperty}&sortOrder={sortOrder}"; | ||
348 | - pageLinkVariables.add(pageLink.getSortOrder().getProperty()); | ||
349 | - pageLinkVariables.add(pageLink.getSortOrder().getDirection().name()); | ||
350 | - } | ||
351 | - | ||
352 | - Object[] vars = new Object[urlVariables.length + pageLinkVariables.size()]; | ||
353 | - System.arraycopy(urlVariables, 0, vars, 0, urlVariables.length); | ||
354 | - System.arraycopy(pageLinkVariables.toArray(), 0, vars, urlVariables.length, pageLinkVariables.size()); | ||
355 | - | ||
356 | - return readResponse(doGet(urlTemplate, vars).andExpect(status().isOk()), responseType); | ||
357 | - } | ||
358 | - | ||
359 | - protected <T> T doGetTypedWithTimePageLink(String urlTemplate, TypeReference<T> responseType, | ||
360 | - TimePageLink pageLink, | ||
361 | - Object... urlVariables) throws Exception { | ||
362 | - List<Object> pageLinkVariables = new ArrayList<>(); | ||
363 | - urlTemplate += "pageSize={pageSize}&page={page}"; | ||
364 | - pageLinkVariables.add(pageLink.getPageSize()); | ||
365 | - pageLinkVariables.add(pageLink.getPage()); | ||
366 | - if (pageLink.getStartTime() != null) { | ||
367 | - urlTemplate += "&startTime={startTime}"; | ||
368 | - pageLinkVariables.add(pageLink.getStartTime()); | ||
369 | - } | ||
370 | - if (pageLink.getEndTime() != null) { | ||
371 | - urlTemplate += "&endTime={endTime}"; | ||
372 | - pageLinkVariables.add(pageLink.getEndTime()); | ||
373 | - } | ||
374 | - if (StringUtils.isNotEmpty(pageLink.getTextSearch())) { | ||
375 | - urlTemplate += "&textSearch={textSearch}"; | ||
376 | - pageLinkVariables.add(pageLink.getTextSearch()); | ||
377 | - } | ||
378 | - if (pageLink.getSortOrder() != null) { | ||
379 | - urlTemplate += "&sortProperty={sortProperty}&sortOrder={sortOrder}"; | ||
380 | - pageLinkVariables.add(pageLink.getSortOrder().getProperty()); | ||
381 | - pageLinkVariables.add(pageLink.getSortOrder().getDirection().name()); | ||
382 | - } | ||
383 | - Object[] vars = new Object[urlVariables.length + pageLinkVariables.size()]; | ||
384 | - System.arraycopy(urlVariables, 0, vars, 0, urlVariables.length); | ||
385 | - System.arraycopy(pageLinkVariables.toArray(), 0, vars, urlVariables.length, pageLinkVariables.size()); | ||
386 | - | ||
387 | - return readResponse(doGet(urlTemplate, vars).andExpect(status().isOk()), responseType); | ||
388 | - } | ||
389 | - | ||
390 | - protected <T> T doPost(String urlTemplate, Class<T> responseClass, String... params) throws Exception { | ||
391 | - return readResponse(doPost(urlTemplate, params).andExpect(status().isOk()), responseClass); | ||
392 | - } | ||
393 | - | ||
394 | - protected <T> T doPost(String urlTemplate, T content, Class<T> responseClass, ResultMatcher resultMatcher, String... params) throws Exception { | ||
395 | - return readResponse(doPost(urlTemplate, content, params).andExpect(resultMatcher), responseClass); | ||
396 | - } | ||
397 | - | ||
398 | - protected <T> T doPost(String urlTemplate, T content, Class<T> responseClass, String... params) throws Exception { | ||
399 | - return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseClass); | ||
400 | - } | ||
401 | - | ||
402 | - protected <T,R> R doPostWithResponse(String urlTemplate, T content, Class<R> responseClass, String... params) throws Exception { | ||
403 | - return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseClass); | ||
404 | - } | ||
405 | - | ||
406 | - protected <T,R> R doPostWithTypedResponse(String urlTemplate, T content, TypeReference<R> responseType, String... params) throws Exception { | ||
407 | - return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseType); | ||
408 | - } | ||
409 | - | ||
410 | - protected <T> T doPostAsync(String urlTemplate, T content, Class<T> responseClass, ResultMatcher resultMatcher, String... params) throws Exception { | ||
411 | - return readResponse(doPostAsync(urlTemplate, content, DEFAULT_TIMEOUT, params).andExpect(resultMatcher), responseClass); | ||
412 | - } | ||
413 | - | ||
414 | - protected <T> T doPostAsync(String urlTemplate, T content, Class<T> responseClass, ResultMatcher resultMatcher, Long timeout, String... params) throws Exception { | ||
415 | - return readResponse(doPostAsync(urlTemplate, content, timeout, params).andExpect(resultMatcher), responseClass); | ||
416 | - } | ||
417 | - | ||
418 | - protected <T> T doDelete(String urlTemplate, Class<T> responseClass, String... params) throws Exception { | ||
419 | - return readResponse(doDelete(urlTemplate, params).andExpect(status().isOk()), responseClass); | ||
420 | - } | ||
421 | - | ||
422 | - protected ResultActions doPost(String urlTemplate, String... params) throws Exception { | ||
423 | - MockHttpServletRequestBuilder postRequest = post(urlTemplate); | ||
424 | - setJwtToken(postRequest); | ||
425 | - populateParams(postRequest, params); | ||
426 | - return mockMvc.perform(postRequest); | ||
427 | - } | ||
428 | - | ||
429 | - protected <T> ResultActions doPost(String urlTemplate, T content, String... params) throws Exception { | ||
430 | - MockHttpServletRequestBuilder postRequest = post(urlTemplate); | ||
431 | - setJwtToken(postRequest); | ||
432 | - String json = json(content); | ||
433 | - postRequest.contentType(contentType).content(json); | ||
434 | - return mockMvc.perform(postRequest); | ||
435 | - } | ||
436 | - | ||
437 | - protected <T> ResultActions doPostAsync(String urlTemplate, T content, Long timeout, String... params) throws Exception { | ||
438 | - MockHttpServletRequestBuilder postRequest = post(urlTemplate); | ||
439 | - setJwtToken(postRequest); | ||
440 | - String json = json(content); | ||
441 | - postRequest.contentType(contentType).content(json); | ||
442 | - MvcResult result = mockMvc.perform(postRequest).andReturn(); | ||
443 | - result.getAsyncResult(timeout); | ||
444 | - return mockMvc.perform(asyncDispatch(result)); | ||
445 | - } | ||
446 | - | ||
447 | - protected ResultActions doDelete(String urlTemplate, String... params) throws Exception { | ||
448 | - MockHttpServletRequestBuilder deleteRequest = delete(urlTemplate); | ||
449 | - setJwtToken(deleteRequest); | ||
450 | - populateParams(deleteRequest, params); | ||
451 | - return mockMvc.perform(deleteRequest); | ||
452 | - } | ||
453 | - | ||
454 | - protected void populateParams(MockHttpServletRequestBuilder request, String... params) { | ||
455 | - if (params != null && params.length > 0) { | ||
456 | - Assert.assertEquals(0, params.length % 2); | ||
457 | - MultiValueMap<String, String> paramsMap = new LinkedMultiValueMap<>(); | ||
458 | - for (int i = 0; i < params.length; i += 2) { | ||
459 | - paramsMap.add(params[i], params[i + 1]); | ||
460 | - } | ||
461 | - request.params(paramsMap); | ||
462 | - } | ||
463 | - } | ||
464 | - | ||
465 | - @SuppressWarnings("unchecked") | ||
466 | - protected String json(Object o) throws IOException { | ||
467 | - MockHttpOutputMessage mockHttpOutputMessage = new MockHttpOutputMessage(); | ||
468 | - | ||
469 | - HttpMessageConverter converter = o instanceof String ? stringHttpMessageConverter : mappingJackson2HttpMessageConverter; | ||
470 | - converter.write(o, MediaType.APPLICATION_JSON, mockHttpOutputMessage); | ||
471 | - return mockHttpOutputMessage.getBodyAsString(); | ||
472 | - } | ||
473 | - | ||
474 | - @SuppressWarnings("unchecked") | ||
475 | - protected <T> T readResponse(ResultActions result, Class<T> responseClass) throws Exception { | ||
476 | - byte[] content = result.andReturn().getResponse().getContentAsByteArray(); | ||
477 | - MockHttpInputMessage mockHttpInputMessage = new MockHttpInputMessage(content); | ||
478 | - HttpMessageConverter converter = responseClass.equals(String.class) ? stringHttpMessageConverter : mappingJackson2HttpMessageConverter; | ||
479 | - return (T) converter.read(responseClass, mockHttpInputMessage); | ||
480 | - } | ||
481 | - | ||
482 | - protected <T> T readResponse(ResultActions result, TypeReference<T> type) throws Exception { | ||
483 | - byte[] content = result.andReturn().getResponse().getContentAsByteArray(); | ||
484 | - ObjectMapper mapper = new ObjectMapper(); | ||
485 | - return mapper.readerFor(type).readValue(content); | ||
486 | - } | ||
487 | - | ||
488 | - public class IdComparator<D extends BaseData<? extends UUIDBased>> implements Comparator<D> { | ||
489 | - @Override | ||
490 | - public int compare(D o1, D o2) { | ||
491 | - return o1.getId().getId().compareTo(o2.getId().getId()); | ||
492 | - } | ||
493 | - } | ||
494 | - | ||
495 | - protected static <T> ResultMatcher statusReason(Matcher<T> matcher) { | ||
496 | - return jsonPath("$.message", matcher); | ||
497 | - } | 96 | +public abstract class AbstractControllerTest extends AbstractWebTest { |
498 | 97 | ||
499 | } | 98 | } |
1 | +/** | ||
2 | + * Copyright © 2016-2020 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.controller; | ||
17 | + | ||
18 | +import com.fasterxml.jackson.core.type.TypeReference; | ||
19 | +import com.fasterxml.jackson.databind.JsonNode; | ||
20 | +import com.fasterxml.jackson.databind.ObjectMapper; | ||
21 | +import io.jsonwebtoken.Claims; | ||
22 | +import io.jsonwebtoken.Header; | ||
23 | +import io.jsonwebtoken.Jwt; | ||
24 | +import io.jsonwebtoken.Jwts; | ||
25 | +import lombok.extern.slf4j.Slf4j; | ||
26 | +import org.apache.commons.lang3.StringUtils; | ||
27 | +import org.hamcrest.Matcher; | ||
28 | +import org.junit.After; | ||
29 | +import org.junit.Assert; | ||
30 | +import org.junit.Before; | ||
31 | +import org.junit.Rule; | ||
32 | +import org.junit.rules.TestRule; | ||
33 | +import org.junit.rules.TestWatcher; | ||
34 | +import org.junit.runner.Description; | ||
35 | +import org.junit.runner.RunWith; | ||
36 | +import org.springframework.beans.factory.annotation.Autowired; | ||
37 | +import org.springframework.boot.test.context.SpringBootContextLoader; | ||
38 | +import org.springframework.boot.test.context.SpringBootTest; | ||
39 | +import org.springframework.context.annotation.ComponentScan; | ||
40 | +import org.springframework.context.annotation.Configuration; | ||
41 | +import org.springframework.http.HttpHeaders; | ||
42 | +import org.springframework.http.MediaType; | ||
43 | +import org.springframework.http.converter.HttpMessageConverter; | ||
44 | +import org.springframework.http.converter.StringHttpMessageConverter; | ||
45 | +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; | ||
46 | +import org.springframework.mock.http.MockHttpInputMessage; | ||
47 | +import org.springframework.mock.http.MockHttpOutputMessage; | ||
48 | +import org.springframework.test.annotation.DirtiesContext; | ||
49 | +import org.springframework.test.context.ActiveProfiles; | ||
50 | +import org.springframework.test.context.ContextConfiguration; | ||
51 | +import org.springframework.test.context.junit4.SpringRunner; | ||
52 | +import org.springframework.test.web.servlet.MockMvc; | ||
53 | +import org.springframework.test.web.servlet.MvcResult; | ||
54 | +import org.springframework.test.web.servlet.ResultActions; | ||
55 | +import org.springframework.test.web.servlet.ResultMatcher; | ||
56 | +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; | ||
57 | +import org.springframework.util.LinkedMultiValueMap; | ||
58 | +import org.springframework.util.MultiValueMap; | ||
59 | +import org.springframework.web.context.WebApplicationContext; | ||
60 | +import org.thingsboard.server.common.data.BaseData; | ||
61 | +import org.thingsboard.server.common.data.Customer; | ||
62 | +import org.thingsboard.server.common.data.Tenant; | ||
63 | +import org.thingsboard.server.common.data.User; | ||
64 | +import org.thingsboard.server.common.data.id.TenantId; | ||
65 | +import org.thingsboard.server.common.data.id.UUIDBased; | ||
66 | +import org.thingsboard.server.common.data.page.PageLink; | ||
67 | +import org.thingsboard.server.common.data.page.TimePageLink; | ||
68 | +import org.thingsboard.server.common.data.security.Authority; | ||
69 | +import org.thingsboard.server.config.ThingsboardSecurityConfiguration; | ||
70 | +import org.thingsboard.server.service.mail.TestMailService; | ||
71 | +import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest; | ||
72 | +import org.thingsboard.server.service.security.auth.rest.LoginRequest; | ||
73 | + | ||
74 | +import java.io.IOException; | ||
75 | +import java.util.ArrayList; | ||
76 | +import java.util.Arrays; | ||
77 | +import java.util.Comparator; | ||
78 | +import java.util.List; | ||
79 | + | ||
80 | +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; | ||
81 | +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; | ||
82 | +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; | ||
83 | +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; | ||
84 | +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; | ||
85 | +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; | ||
86 | +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; | ||
87 | +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; | ||
88 | +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; | ||
89 | +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; | ||
90 | + | ||
91 | +@Slf4j | ||
92 | +public abstract class AbstractWebTest { | ||
93 | + | ||
94 | + protected ObjectMapper mapper = new ObjectMapper(); | ||
95 | + | ||
96 | + protected static final String TEST_TENANT_NAME = "TEST TENANT"; | ||
97 | + | ||
98 | + protected static final String SYS_ADMIN_EMAIL = "sysadmin@thingsboard.org"; | ||
99 | + private static final String SYS_ADMIN_PASSWORD = "sysadmin"; | ||
100 | + | ||
101 | + protected static final String TENANT_ADMIN_EMAIL = "testtenant@thingsboard.org"; | ||
102 | + private static final String TENANT_ADMIN_PASSWORD = "tenant"; | ||
103 | + | ||
104 | + protected static final String CUSTOMER_USER_EMAIL = "testcustomer@thingsboard.org"; | ||
105 | + private static final String CUSTOMER_USER_PASSWORD = "customer"; | ||
106 | + | ||
107 | + /** See {@link org.springframework.test.web.servlet.DefaultMvcResult#getAsyncResult(long)} | ||
108 | + * and {@link org.springframework.mock.web.MockAsyncContext#getTimeout()} | ||
109 | + */ | ||
110 | + private static final long DEFAULT_TIMEOUT = -1L; | ||
111 | + | ||
112 | + protected MediaType contentType = MediaType.APPLICATION_JSON; | ||
113 | + | ||
114 | + protected MockMvc mockMvc; | ||
115 | + | ||
116 | + protected String token; | ||
117 | + protected String refreshToken; | ||
118 | + protected String username; | ||
119 | + | ||
120 | + private TenantId tenantId; | ||
121 | + | ||
122 | + @SuppressWarnings("rawtypes") | ||
123 | + private HttpMessageConverter mappingJackson2HttpMessageConverter; | ||
124 | + | ||
125 | + @SuppressWarnings("rawtypes") | ||
126 | + private HttpMessageConverter stringHttpMessageConverter; | ||
127 | + | ||
128 | + @Autowired | ||
129 | + private WebApplicationContext webApplicationContext; | ||
130 | + | ||
131 | + @Rule | ||
132 | + public TestRule watcher = new TestWatcher() { | ||
133 | + protected void starting(Description description) { | ||
134 | + log.info("Starting test: {}", description.getMethodName()); | ||
135 | + } | ||
136 | + | ||
137 | + protected void finished(Description description) { | ||
138 | + log.info("Finished test: {}", description.getMethodName()); | ||
139 | + } | ||
140 | + }; | ||
141 | + | ||
142 | + @Autowired | ||
143 | + void setConverters(HttpMessageConverter<?>[] converters) { | ||
144 | + | ||
145 | + this.mappingJackson2HttpMessageConverter = Arrays.stream(converters) | ||
146 | + .filter(hmc -> hmc instanceof MappingJackson2HttpMessageConverter) | ||
147 | + .findAny() | ||
148 | + .get(); | ||
149 | + | ||
150 | + this.stringHttpMessageConverter = Arrays.stream(converters) | ||
151 | + .filter(hmc -> hmc instanceof StringHttpMessageConverter) | ||
152 | + .findAny() | ||
153 | + .get(); | ||
154 | + | ||
155 | + Assert.assertNotNull("the JSON message converter must not be null", | ||
156 | + this.mappingJackson2HttpMessageConverter); | ||
157 | + } | ||
158 | + | ||
159 | + @Before | ||
160 | + public void setup() throws Exception { | ||
161 | + log.info("Executing setup"); | ||
162 | + if (this.mockMvc == null) { | ||
163 | + this.mockMvc = webAppContextSetup(webApplicationContext) | ||
164 | + .apply(springSecurity()).build(); | ||
165 | + } | ||
166 | + loginSysAdmin(); | ||
167 | + | ||
168 | + Tenant tenant = new Tenant(); | ||
169 | + tenant.setTitle(TEST_TENANT_NAME); | ||
170 | + Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class); | ||
171 | + Assert.assertNotNull(savedTenant); | ||
172 | + tenantId = savedTenant.getId(); | ||
173 | + | ||
174 | + User tenantAdmin = new User(); | ||
175 | + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); | ||
176 | + tenantAdmin.setTenantId(tenantId); | ||
177 | + tenantAdmin.setEmail(TENANT_ADMIN_EMAIL); | ||
178 | + | ||
179 | + createUserAndLogin(tenantAdmin, TENANT_ADMIN_PASSWORD); | ||
180 | + | ||
181 | + Customer customer = new Customer(); | ||
182 | + customer.setTitle("Customer"); | ||
183 | + customer.setTenantId(tenantId); | ||
184 | + Customer savedCustomer = doPost("/api/customer", customer, Customer.class); | ||
185 | + | ||
186 | + User customerUser = new User(); | ||
187 | + customerUser.setAuthority(Authority.CUSTOMER_USER); | ||
188 | + customerUser.setTenantId(tenantId); | ||
189 | + customerUser.setCustomerId(savedCustomer.getId()); | ||
190 | + customerUser.setEmail(CUSTOMER_USER_EMAIL); | ||
191 | + | ||
192 | + createUserAndLogin(customerUser, CUSTOMER_USER_PASSWORD); | ||
193 | + | ||
194 | + logout(); | ||
195 | + | ||
196 | + log.info("Executed setup"); | ||
197 | + } | ||
198 | + | ||
199 | + @After | ||
200 | + public void teardown() throws Exception { | ||
201 | + log.info("Executing teardown"); | ||
202 | + loginSysAdmin(); | ||
203 | + doDelete("/api/tenant/" + tenantId.getId().toString()) | ||
204 | + .andExpect(status().isOk()); | ||
205 | + log.info("Executed teardown"); | ||
206 | + } | ||
207 | + | ||
208 | + protected void loginSysAdmin() throws Exception { | ||
209 | + login(SYS_ADMIN_EMAIL, SYS_ADMIN_PASSWORD); | ||
210 | + } | ||
211 | + | ||
212 | + protected void loginTenantAdmin() throws Exception { | ||
213 | + login(TENANT_ADMIN_EMAIL, TENANT_ADMIN_PASSWORD); | ||
214 | + } | ||
215 | + | ||
216 | + protected void loginCustomerUser() throws Exception { | ||
217 | + login(CUSTOMER_USER_EMAIL, CUSTOMER_USER_PASSWORD); | ||
218 | + } | ||
219 | + | ||
220 | + private Tenant savedDifferentTenant; | ||
221 | + protected void loginDifferentTenant() throws Exception { | ||
222 | + loginSysAdmin(); | ||
223 | + Tenant tenant = new Tenant(); | ||
224 | + tenant.setTitle("Different tenant"); | ||
225 | + savedDifferentTenant = doPost("/api/tenant", tenant, Tenant.class); | ||
226 | + Assert.assertNotNull(savedDifferentTenant); | ||
227 | + User differentTenantAdmin = new User(); | ||
228 | + differentTenantAdmin.setAuthority(Authority.TENANT_ADMIN); | ||
229 | + differentTenantAdmin.setTenantId(savedDifferentTenant.getId()); | ||
230 | + differentTenantAdmin.setEmail("different_tenant@thingsboard.org"); | ||
231 | + | ||
232 | + createUserAndLogin(differentTenantAdmin, "testPassword"); | ||
233 | + } | ||
234 | + | ||
235 | + protected void deleteDifferentTenant() throws Exception { | ||
236 | + loginSysAdmin(); | ||
237 | + doDelete("/api/tenant/" + savedDifferentTenant.getId().getId().toString()) | ||
238 | + .andExpect(status().isOk()); | ||
239 | + } | ||
240 | + | ||
241 | + protected User createUserAndLogin(User user, String password) throws Exception { | ||
242 | + User savedUser = doPost("/api/user", user, User.class); | ||
243 | + logout(); | ||
244 | + doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken) | ||
245 | + .andExpect(status().isSeeOther()) | ||
246 | + .andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken)); | ||
247 | + JsonNode activateRequest = new ObjectMapper().createObjectNode() | ||
248 | + .put("activateToken", TestMailService.currentActivateToken) | ||
249 | + .put("password", password); | ||
250 | + JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class); | ||
251 | + validateAndSetJwtToken(tokenInfo, user.getEmail()); | ||
252 | + return savedUser; | ||
253 | + } | ||
254 | + | ||
255 | + protected void login(String username, String password) throws Exception { | ||
256 | + this.token = null; | ||
257 | + this.refreshToken = null; | ||
258 | + this.username = null; | ||
259 | + JsonNode tokenInfo = readResponse(doPost("/api/auth/login", new LoginRequest(username, password)).andExpect(status().isOk()), JsonNode.class); | ||
260 | + validateAndSetJwtToken(tokenInfo, username); | ||
261 | + } | ||
262 | + | ||
263 | + protected void refreshToken() throws Exception { | ||
264 | + this.token = null; | ||
265 | + JsonNode tokenInfo = readResponse(doPost("/api/auth/token", new RefreshTokenRequest(this.refreshToken)).andExpect(status().isOk()), JsonNode.class); | ||
266 | + validateAndSetJwtToken(tokenInfo, this.username); | ||
267 | + } | ||
268 | + | ||
269 | + protected void validateAndSetJwtToken(JsonNode tokenInfo, String username) { | ||
270 | + Assert.assertNotNull(tokenInfo); | ||
271 | + Assert.assertTrue(tokenInfo.has("token")); | ||
272 | + Assert.assertTrue(tokenInfo.has("refreshToken")); | ||
273 | + String token = tokenInfo.get("token").asText(); | ||
274 | + String refreshToken = tokenInfo.get("refreshToken").asText(); | ||
275 | + validateJwtToken(token, username); | ||
276 | + validateJwtToken(refreshToken, username); | ||
277 | + this.token = token; | ||
278 | + this.refreshToken = refreshToken; | ||
279 | + this.username = username; | ||
280 | + } | ||
281 | + | ||
282 | + protected void validateJwtToken(String token, String username) { | ||
283 | + Assert.assertNotNull(token); | ||
284 | + Assert.assertFalse(token.isEmpty()); | ||
285 | + int i = token.lastIndexOf('.'); | ||
286 | + Assert.assertTrue(i > 0); | ||
287 | + String withoutSignature = token.substring(0, i + 1); | ||
288 | + Jwt<Header, Claims> jwsClaims = Jwts.parser().parseClaimsJwt(withoutSignature); | ||
289 | + Claims claims = jwsClaims.getBody(); | ||
290 | + String subject = claims.getSubject(); | ||
291 | + Assert.assertEquals(username, subject); | ||
292 | + } | ||
293 | + | ||
294 | + protected void logout() throws Exception { | ||
295 | + this.token = null; | ||
296 | + this.refreshToken = null; | ||
297 | + this.username = null; | ||
298 | + } | ||
299 | + | ||
300 | + protected void setJwtToken(MockHttpServletRequestBuilder request) { | ||
301 | + if (this.token != null) { | ||
302 | + request.header(ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM, "Bearer " + this.token); | ||
303 | + } | ||
304 | + } | ||
305 | + | ||
306 | + protected ResultActions doGet(String urlTemplate, Object... urlVariables) throws Exception { | ||
307 | + MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables); | ||
308 | + setJwtToken(getRequest); | ||
309 | + return mockMvc.perform(getRequest); | ||
310 | + } | ||
311 | + | ||
312 | + protected <T> T doGet(String urlTemplate, Class<T> responseClass, Object... urlVariables) throws Exception { | ||
313 | + return readResponse(doGet(urlTemplate, urlVariables).andExpect(status().isOk()), responseClass); | ||
314 | + } | ||
315 | + | ||
316 | + protected <T> T doGetAsync(String urlTemplate, Class<T> responseClass, Object... urlVariables) throws Exception { | ||
317 | + return readResponse(doGetAsync(urlTemplate, urlVariables).andExpect(status().isOk()), responseClass); | ||
318 | + } | ||
319 | + | ||
320 | + protected ResultActions doGetAsync(String urlTemplate, Object... urlVariables) throws Exception { | ||
321 | + MockHttpServletRequestBuilder getRequest; | ||
322 | + getRequest = get(urlTemplate, urlVariables); | ||
323 | + setJwtToken(getRequest); | ||
324 | + return mockMvc.perform(asyncDispatch(mockMvc.perform(getRequest).andExpect(request().asyncStarted()).andReturn())); | ||
325 | + } | ||
326 | + | ||
327 | + protected <T> T doGetTyped(String urlTemplate, TypeReference<T> responseType, Object... urlVariables) throws Exception { | ||
328 | + return readResponse(doGet(urlTemplate, urlVariables).andExpect(status().isOk()), responseType); | ||
329 | + } | ||
330 | + | ||
331 | + protected <T> T doGetTypedWithPageLink(String urlTemplate, TypeReference<T> responseType, | ||
332 | + PageLink pageLink, | ||
333 | + Object... urlVariables) throws Exception { | ||
334 | + List<Object> pageLinkVariables = new ArrayList<>(); | ||
335 | + urlTemplate += "pageSize={pageSize}&page={page}"; | ||
336 | + pageLinkVariables.add(pageLink.getPageSize()); | ||
337 | + pageLinkVariables.add(pageLink.getPage()); | ||
338 | + if (StringUtils.isNotEmpty(pageLink.getTextSearch())) { | ||
339 | + urlTemplate += "&textSearch={textSearch}"; | ||
340 | + pageLinkVariables.add(pageLink.getTextSearch()); | ||
341 | + } | ||
342 | + if (pageLink.getSortOrder() != null) { | ||
343 | + urlTemplate += "&sortProperty={sortProperty}&sortOrder={sortOrder}"; | ||
344 | + pageLinkVariables.add(pageLink.getSortOrder().getProperty()); | ||
345 | + pageLinkVariables.add(pageLink.getSortOrder().getDirection().name()); | ||
346 | + } | ||
347 | + | ||
348 | + Object[] vars = new Object[urlVariables.length + pageLinkVariables.size()]; | ||
349 | + System.arraycopy(urlVariables, 0, vars, 0, urlVariables.length); | ||
350 | + System.arraycopy(pageLinkVariables.toArray(), 0, vars, urlVariables.length, pageLinkVariables.size()); | ||
351 | + | ||
352 | + return readResponse(doGet(urlTemplate, vars).andExpect(status().isOk()), responseType); | ||
353 | + } | ||
354 | + | ||
355 | + protected <T> T doGetTypedWithTimePageLink(String urlTemplate, TypeReference<T> responseType, | ||
356 | + TimePageLink pageLink, | ||
357 | + Object... urlVariables) throws Exception { | ||
358 | + List<Object> pageLinkVariables = new ArrayList<>(); | ||
359 | + urlTemplate += "pageSize={pageSize}&page={page}"; | ||
360 | + pageLinkVariables.add(pageLink.getPageSize()); | ||
361 | + pageLinkVariables.add(pageLink.getPage()); | ||
362 | + if (pageLink.getStartTime() != null) { | ||
363 | + urlTemplate += "&startTime={startTime}"; | ||
364 | + pageLinkVariables.add(pageLink.getStartTime()); | ||
365 | + } | ||
366 | + if (pageLink.getEndTime() != null) { | ||
367 | + urlTemplate += "&endTime={endTime}"; | ||
368 | + pageLinkVariables.add(pageLink.getEndTime()); | ||
369 | + } | ||
370 | + if (StringUtils.isNotEmpty(pageLink.getTextSearch())) { | ||
371 | + urlTemplate += "&textSearch={textSearch}"; | ||
372 | + pageLinkVariables.add(pageLink.getTextSearch()); | ||
373 | + } | ||
374 | + if (pageLink.getSortOrder() != null) { | ||
375 | + urlTemplate += "&sortProperty={sortProperty}&sortOrder={sortOrder}"; | ||
376 | + pageLinkVariables.add(pageLink.getSortOrder().getProperty()); | ||
377 | + pageLinkVariables.add(pageLink.getSortOrder().getDirection().name()); | ||
378 | + } | ||
379 | + Object[] vars = new Object[urlVariables.length + pageLinkVariables.size()]; | ||
380 | + System.arraycopy(urlVariables, 0, vars, 0, urlVariables.length); | ||
381 | + System.arraycopy(pageLinkVariables.toArray(), 0, vars, urlVariables.length, pageLinkVariables.size()); | ||
382 | + | ||
383 | + return readResponse(doGet(urlTemplate, vars).andExpect(status().isOk()), responseType); | ||
384 | + } | ||
385 | + | ||
386 | + protected <T> T doPost(String urlTemplate, Class<T> responseClass, String... params) throws Exception { | ||
387 | + return readResponse(doPost(urlTemplate, params).andExpect(status().isOk()), responseClass); | ||
388 | + } | ||
389 | + | ||
390 | + protected <T> T doPost(String urlTemplate, T content, Class<T> responseClass, ResultMatcher resultMatcher, String... params) throws Exception { | ||
391 | + return readResponse(doPost(urlTemplate, content, params).andExpect(resultMatcher), responseClass); | ||
392 | + } | ||
393 | + | ||
394 | + protected <T> T doPost(String urlTemplate, T content, Class<T> responseClass, String... params) throws Exception { | ||
395 | + return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseClass); | ||
396 | + } | ||
397 | + | ||
398 | + protected <T,R> R doPostWithResponse(String urlTemplate, T content, Class<R> responseClass, String... params) throws Exception { | ||
399 | + return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseClass); | ||
400 | + } | ||
401 | + | ||
402 | + protected <T,R> R doPostWithTypedResponse(String urlTemplate, T content, TypeReference<R> responseType, String... params) throws Exception { | ||
403 | + return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseType); | ||
404 | + } | ||
405 | + | ||
406 | + protected <T> T doPostAsync(String urlTemplate, T content, Class<T> responseClass, ResultMatcher resultMatcher, String... params) throws Exception { | ||
407 | + return readResponse(doPostAsync(urlTemplate, content, DEFAULT_TIMEOUT, params).andExpect(resultMatcher), responseClass); | ||
408 | + } | ||
409 | + | ||
410 | + protected <T> T doPostAsync(String urlTemplate, T content, Class<T> responseClass, ResultMatcher resultMatcher, Long timeout, String... params) throws Exception { | ||
411 | + return readResponse(doPostAsync(urlTemplate, content, timeout, params).andExpect(resultMatcher), responseClass); | ||
412 | + } | ||
413 | + | ||
414 | + protected <T> T doDelete(String urlTemplate, Class<T> responseClass, String... params) throws Exception { | ||
415 | + return readResponse(doDelete(urlTemplate, params).andExpect(status().isOk()), responseClass); | ||
416 | + } | ||
417 | + | ||
418 | + protected ResultActions doPost(String urlTemplate, String... params) throws Exception { | ||
419 | + MockHttpServletRequestBuilder postRequest = post(urlTemplate); | ||
420 | + setJwtToken(postRequest); | ||
421 | + populateParams(postRequest, params); | ||
422 | + return mockMvc.perform(postRequest); | ||
423 | + } | ||
424 | + | ||
425 | + protected <T> ResultActions doPost(String urlTemplate, T content, String... params) throws Exception { | ||
426 | + MockHttpServletRequestBuilder postRequest = post(urlTemplate); | ||
427 | + setJwtToken(postRequest); | ||
428 | + String json = json(content); | ||
429 | + postRequest.contentType(contentType).content(json); | ||
430 | + return mockMvc.perform(postRequest); | ||
431 | + } | ||
432 | + | ||
433 | + protected <T> ResultActions doPostAsync(String urlTemplate, T content, Long timeout, String... params) throws Exception { | ||
434 | + MockHttpServletRequestBuilder postRequest = post(urlTemplate); | ||
435 | + setJwtToken(postRequest); | ||
436 | + String json = json(content); | ||
437 | + postRequest.contentType(contentType).content(json); | ||
438 | + MvcResult result = mockMvc.perform(postRequest).andReturn(); | ||
439 | + result.getAsyncResult(timeout); | ||
440 | + return mockMvc.perform(asyncDispatch(result)); | ||
441 | + } | ||
442 | + | ||
443 | + protected ResultActions doDelete(String urlTemplate, String... params) throws Exception { | ||
444 | + MockHttpServletRequestBuilder deleteRequest = delete(urlTemplate); | ||
445 | + setJwtToken(deleteRequest); | ||
446 | + populateParams(deleteRequest, params); | ||
447 | + return mockMvc.perform(deleteRequest); | ||
448 | + } | ||
449 | + | ||
450 | + protected void populateParams(MockHttpServletRequestBuilder request, String... params) { | ||
451 | + if (params != null && params.length > 0) { | ||
452 | + Assert.assertEquals(0, params.length % 2); | ||
453 | + MultiValueMap<String, String> paramsMap = new LinkedMultiValueMap<>(); | ||
454 | + for (int i = 0; i < params.length; i += 2) { | ||
455 | + paramsMap.add(params[i], params[i + 1]); | ||
456 | + } | ||
457 | + request.params(paramsMap); | ||
458 | + } | ||
459 | + } | ||
460 | + | ||
461 | + @SuppressWarnings("unchecked") | ||
462 | + protected String json(Object o) throws IOException { | ||
463 | + MockHttpOutputMessage mockHttpOutputMessage = new MockHttpOutputMessage(); | ||
464 | + | ||
465 | + HttpMessageConverter converter = o instanceof String ? stringHttpMessageConverter : mappingJackson2HttpMessageConverter; | ||
466 | + converter.write(o, MediaType.APPLICATION_JSON, mockHttpOutputMessage); | ||
467 | + return mockHttpOutputMessage.getBodyAsString(); | ||
468 | + } | ||
469 | + | ||
470 | + @SuppressWarnings("unchecked") | ||
471 | + protected <T> T readResponse(ResultActions result, Class<T> responseClass) throws Exception { | ||
472 | + byte[] content = result.andReturn().getResponse().getContentAsByteArray(); | ||
473 | + MockHttpInputMessage mockHttpInputMessage = new MockHttpInputMessage(content); | ||
474 | + HttpMessageConverter converter = responseClass.equals(String.class) ? stringHttpMessageConverter : mappingJackson2HttpMessageConverter; | ||
475 | + return (T) converter.read(responseClass, mockHttpInputMessage); | ||
476 | + } | ||
477 | + | ||
478 | + protected <T> T readResponse(ResultActions result, TypeReference<T> type) throws Exception { | ||
479 | + byte[] content = result.andReturn().getResponse().getContentAsByteArray(); | ||
480 | + ObjectMapper mapper = new ObjectMapper(); | ||
481 | + return mapper.readerFor(type).readValue(content); | ||
482 | + } | ||
483 | + | ||
484 | + public class IdComparator<D extends BaseData<? extends UUIDBased>> implements Comparator<D> { | ||
485 | + @Override | ||
486 | + public int compare(D o1, D o2) { | ||
487 | + return o1.getId().getId().compareTo(o2.getId().getId()); | ||
488 | + } | ||
489 | + } | ||
490 | + | ||
491 | + protected static <T> ResultMatcher statusReason(Matcher<T> matcher) { | ||
492 | + return jsonPath("$.message", matcher); | ||
493 | + } | ||
494 | + | ||
495 | +} |
1 | +/** | ||
2 | + * Copyright © 2016-2020 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.controller; | ||
17 | + | ||
18 | +import com.fasterxml.jackson.core.type.TypeReference; | ||
19 | +import com.fasterxml.jackson.databind.JsonNode; | ||
20 | +import com.fasterxml.jackson.databind.ObjectMapper; | ||
21 | +import io.jsonwebtoken.Claims; | ||
22 | +import io.jsonwebtoken.Header; | ||
23 | +import io.jsonwebtoken.Jwt; | ||
24 | +import io.jsonwebtoken.Jwts; | ||
25 | +import lombok.extern.slf4j.Slf4j; | ||
26 | +import org.apache.commons.lang3.StringUtils; | ||
27 | +import org.hamcrest.Matcher; | ||
28 | +import org.junit.After; | ||
29 | +import org.junit.Assert; | ||
30 | +import org.junit.Before; | ||
31 | +import org.junit.Rule; | ||
32 | +import org.junit.rules.TestRule; | ||
33 | +import org.junit.rules.TestWatcher; | ||
34 | +import org.junit.runner.Description; | ||
35 | +import org.junit.runner.RunWith; | ||
36 | +import org.springframework.beans.factory.annotation.Autowired; | ||
37 | +import org.springframework.boot.test.context.SpringBootContextLoader; | ||
38 | +import org.springframework.boot.test.context.SpringBootTest; | ||
39 | +import org.springframework.boot.web.server.LocalServerPort; | ||
40 | +import org.springframework.context.annotation.ComponentScan; | ||
41 | +import org.springframework.context.annotation.Configuration; | ||
42 | +import org.springframework.http.HttpHeaders; | ||
43 | +import org.springframework.http.MediaType; | ||
44 | +import org.springframework.http.converter.HttpMessageConverter; | ||
45 | +import org.springframework.http.converter.StringHttpMessageConverter; | ||
46 | +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; | ||
47 | +import org.springframework.mock.http.MockHttpInputMessage; | ||
48 | +import org.springframework.mock.http.MockHttpOutputMessage; | ||
49 | +import org.springframework.test.annotation.DirtiesContext; | ||
50 | +import org.springframework.test.context.ActiveProfiles; | ||
51 | +import org.springframework.test.context.ContextConfiguration; | ||
52 | +import org.springframework.test.context.junit4.SpringRunner; | ||
53 | +import org.springframework.test.web.servlet.MockMvc; | ||
54 | +import org.springframework.test.web.servlet.MvcResult; | ||
55 | +import org.springframework.test.web.servlet.ResultActions; | ||
56 | +import org.springframework.test.web.servlet.ResultMatcher; | ||
57 | +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; | ||
58 | +import org.springframework.util.LinkedMultiValueMap; | ||
59 | +import org.springframework.util.MultiValueMap; | ||
60 | +import org.springframework.web.context.WebApplicationContext; | ||
61 | +import org.thingsboard.server.common.data.BaseData; | ||
62 | +import org.thingsboard.server.common.data.Customer; | ||
63 | +import org.thingsboard.server.common.data.Tenant; | ||
64 | +import org.thingsboard.server.common.data.User; | ||
65 | +import org.thingsboard.server.common.data.id.TenantId; | ||
66 | +import org.thingsboard.server.common.data.id.UUIDBased; | ||
67 | +import org.thingsboard.server.common.data.page.PageLink; | ||
68 | +import org.thingsboard.server.common.data.page.TimePageLink; | ||
69 | +import org.thingsboard.server.common.data.security.Authority; | ||
70 | +import org.thingsboard.server.config.ThingsboardSecurityConfiguration; | ||
71 | +import org.thingsboard.server.service.mail.TestMailService; | ||
72 | +import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest; | ||
73 | +import org.thingsboard.server.service.security.auth.rest.LoginRequest; | ||
74 | + | ||
75 | +import java.io.IOException; | ||
76 | +import java.net.URI; | ||
77 | +import java.net.URISyntaxException; | ||
78 | +import java.util.ArrayList; | ||
79 | +import java.util.Arrays; | ||
80 | +import java.util.Comparator; | ||
81 | +import java.util.List; | ||
82 | + | ||
83 | +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; | ||
84 | +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; | ||
85 | +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; | ||
86 | +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; | ||
87 | +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; | ||
88 | +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; | ||
89 | +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; | ||
90 | +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; | ||
91 | +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; | ||
92 | +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; | ||
93 | + | ||
94 | +@ActiveProfiles("test") | ||
95 | +@RunWith(SpringRunner.class) | ||
96 | +@ContextConfiguration(classes = AbstractControllerTest.class, loader = SpringBootContextLoader.class) | ||
97 | +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) | ||
98 | +@Configuration | ||
99 | +@ComponentScan({"org.thingsboard.server"}) | ||
100 | +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) | ||
101 | +@Slf4j | ||
102 | +public abstract class AbstractWebsocketTest extends AbstractWebTest { | ||
103 | + | ||
104 | + protected static final String WS_URL = "ws://localhost:"; | ||
105 | + | ||
106 | + @LocalServerPort | ||
107 | + protected int wsPort; | ||
108 | + | ||
109 | + protected TbTestWebSocketClient buildAndConnectWebSocketClient() throws URISyntaxException, InterruptedException { | ||
110 | + TbTestWebSocketClient wsClient = new TbTestWebSocketClient(new URI(WS_URL + wsPort + "/api/ws/plugins/telemetry?token=" + token)); | ||
111 | + Assert.assertTrue(wsClient.connectBlocking()); | ||
112 | + return wsClient; | ||
113 | + } | ||
114 | + | ||
115 | +} |
@@ -16,10 +16,16 @@ | @@ -16,10 +16,16 @@ | ||
16 | package org.thingsboard.server.controller; | 16 | package org.thingsboard.server.controller; |
17 | 17 | ||
18 | import com.fasterxml.jackson.core.type.TypeReference; | 18 | import com.fasterxml.jackson.core.type.TypeReference; |
19 | +import com.google.gson.JsonArray; | ||
20 | +import com.google.gson.JsonObject; | ||
21 | +import org.apache.http.conn.ssl.TrustStrategy; | ||
22 | +import org.apache.http.ssl.SSLContextBuilder; | ||
23 | +import org.apache.http.ssl.SSLContexts; | ||
19 | import org.junit.After; | 24 | import org.junit.After; |
20 | import org.junit.Assert; | 25 | import org.junit.Assert; |
21 | import org.junit.Before; | 26 | import org.junit.Before; |
22 | import org.junit.Test; | 27 | import org.junit.Test; |
28 | +import org.springframework.boot.web.server.LocalServerPort; | ||
23 | import org.thingsboard.server.common.data.DataConstants; | 29 | import org.thingsboard.server.common.data.DataConstants; |
24 | import org.thingsboard.server.common.data.Device; | 30 | import org.thingsboard.server.common.data.Device; |
25 | import org.thingsboard.server.common.data.EntityType; | 31 | import org.thingsboard.server.common.data.EntityType; |
@@ -27,6 +33,7 @@ import org.thingsboard.server.common.data.Tenant; | @@ -27,6 +33,7 @@ import org.thingsboard.server.common.data.Tenant; | ||
27 | import org.thingsboard.server.common.data.User; | 33 | import org.thingsboard.server.common.data.User; |
28 | import org.thingsboard.server.common.data.id.DeviceId; | 34 | import org.thingsboard.server.common.data.id.DeviceId; |
29 | import org.thingsboard.server.common.data.id.EntityId; | 35 | import org.thingsboard.server.common.data.id.EntityId; |
36 | +import org.thingsboard.server.common.data.kv.Aggregation; | ||
30 | import org.thingsboard.server.common.data.page.PageData; | 37 | import org.thingsboard.server.common.data.page.PageData; |
31 | import org.thingsboard.server.common.data.query.DeviceTypeFilter; | 38 | import org.thingsboard.server.common.data.query.DeviceTypeFilter; |
32 | import org.thingsboard.server.common.data.query.EntityCountQuery; | 39 | import org.thingsboard.server.common.data.query.EntityCountQuery; |
@@ -40,10 +47,16 @@ import org.thingsboard.server.common.data.query.EntityListFilter; | @@ -40,10 +47,16 @@ import org.thingsboard.server.common.data.query.EntityListFilter; | ||
40 | import org.thingsboard.server.common.data.query.KeyFilter; | 47 | import org.thingsboard.server.common.data.query.KeyFilter; |
41 | import org.thingsboard.server.common.data.query.NumericFilterPredicate; | 48 | import org.thingsboard.server.common.data.query.NumericFilterPredicate; |
42 | import org.thingsboard.server.common.data.security.Authority; | 49 | import org.thingsboard.server.common.data.security.Authority; |
50 | +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; | ||
51 | +import org.thingsboard.server.service.telemetry.cmd.v2.EntityHistoryCmd; | ||
43 | 52 | ||
53 | +import java.net.URI; | ||
54 | +import java.net.URISyntaxException; | ||
44 | import java.util.ArrayList; | 55 | import java.util.ArrayList; |
56 | +import java.util.Arrays; | ||
45 | import java.util.Collections; | 57 | import java.util.Collections; |
46 | import java.util.List; | 58 | import java.util.List; |
59 | +import java.util.Random; | ||
47 | import java.util.stream.Collectors; | 60 | import java.util.stream.Collectors; |
48 | 61 | ||
49 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; | 62 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
@@ -190,23 +203,23 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe | @@ -190,23 +203,23 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe | ||
190 | List<Device> devices = new ArrayList<>(); | 203 | List<Device> devices = new ArrayList<>(); |
191 | List<Long> temperatures = new ArrayList<>(); | 204 | List<Long> temperatures = new ArrayList<>(); |
192 | List<Long> highTemperatures = new ArrayList<>(); | 205 | List<Long> highTemperatures = new ArrayList<>(); |
193 | - for (int i=0;i<67;i++) { | 206 | + for (int i = 0; i < 67; i++) { |
194 | Device device = new Device(); | 207 | Device device = new Device(); |
195 | - String name = "Device"+i; | 208 | + String name = "Device" + i; |
196 | device.setName(name); | 209 | device.setName(name); |
197 | device.setType("default"); | 210 | device.setType("default"); |
198 | - device.setLabel("testLabel"+(int)(Math.random()*1000)); | ||
199 | - devices.add(doPost("/api/device?accessToken="+name, device, Device.class)); | ||
200 | - long temperature = (long)(Math.random()*100); | 211 | + device.setLabel("testLabel" + (int) (Math.random() * 1000)); |
212 | + devices.add(doPost("/api/device?accessToken=" + name, device, Device.class)); | ||
213 | + long temperature = (long) (Math.random() * 100); | ||
201 | temperatures.add(temperature); | 214 | temperatures.add(temperature); |
202 | if (temperature > 45) { | 215 | if (temperature > 45) { |
203 | highTemperatures.add(temperature); | 216 | highTemperatures.add(temperature); |
204 | } | 217 | } |
205 | } | 218 | } |
206 | - for (int i=0;i<devices.size();i++) { | 219 | + for (int i = 0; i < devices.size(); i++) { |
207 | Device device = devices.get(i); | 220 | Device device = devices.get(i); |
208 | - String payload = "{\"temperature\":"+temperatures.get(i)+"}"; | ||
209 | - doPost("/api/plugins/telemetry/"+device.getId()+"/"+ DataConstants.SHARED_SCOPE, payload, String.class, status().isOk()); | 221 | + String payload = "{\"temperature\":" + temperatures.get(i) + "}"; |
222 | + doPost("/api/plugins/telemetry/" + device.getId() + "/" + DataConstants.SHARED_SCOPE, payload, String.class, status().isOk()); | ||
210 | } | 223 | } |
211 | Thread.sleep(1000); | 224 | Thread.sleep(1000); |
212 | 225 |
1 | +/** | ||
2 | + * Copyright © 2016-2020 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.controller; | ||
17 | + | ||
18 | +import org.junit.After; | ||
19 | +import org.junit.Assert; | ||
20 | +import org.junit.Before; | ||
21 | +import org.junit.Test; | ||
22 | +import org.springframework.beans.factory.annotation.Autowired; | ||
23 | +import org.thingsboard.server.common.data.Device; | ||
24 | +import org.thingsboard.server.common.data.Tenant; | ||
25 | +import org.thingsboard.server.common.data.User; | ||
26 | +import org.thingsboard.server.common.data.kv.Aggregation; | ||
27 | +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; | ||
28 | +import org.thingsboard.server.common.data.kv.LongDataEntry; | ||
29 | +import org.thingsboard.server.common.data.kv.TsKvEntry; | ||
30 | +import org.thingsboard.server.common.data.page.PageData; | ||
31 | +import org.thingsboard.server.common.data.query.DeviceTypeFilter; | ||
32 | +import org.thingsboard.server.common.data.query.EntityData; | ||
33 | +import org.thingsboard.server.common.data.query.EntityDataPageLink; | ||
34 | +import org.thingsboard.server.common.data.query.EntityDataQuery; | ||
35 | +import org.thingsboard.server.common.data.query.TsValue; | ||
36 | +import org.thingsboard.server.common.data.security.Authority; | ||
37 | +import org.thingsboard.server.dao.timeseries.TimeseriesService; | ||
38 | +import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper; | ||
39 | +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; | ||
40 | +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; | ||
41 | +import org.thingsboard.server.service.telemetry.cmd.v2.EntityHistoryCmd; | ||
42 | + | ||
43 | +import java.util.Arrays; | ||
44 | +import java.util.Collections; | ||
45 | +import java.util.concurrent.TimeUnit; | ||
46 | + | ||
47 | +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; | ||
48 | + | ||
49 | +public class BaseWebsocketApiTest extends AbstractWebsocketTest { | ||
50 | + | ||
51 | + private Tenant savedTenant; | ||
52 | + private User tenantAdmin; | ||
53 | + private TbTestWebSocketClient wsClient; | ||
54 | + | ||
55 | + @Autowired | ||
56 | + private TimeseriesService tsService; | ||
57 | + | ||
58 | + @Before | ||
59 | + public void beforeTest() throws Exception { | ||
60 | + loginSysAdmin(); | ||
61 | + | ||
62 | + Tenant tenant = new Tenant(); | ||
63 | + tenant.setTitle("My tenant"); | ||
64 | + savedTenant = doPost("/api/tenant", tenant, Tenant.class); | ||
65 | + Assert.assertNotNull(savedTenant); | ||
66 | + | ||
67 | + tenantAdmin = new User(); | ||
68 | + tenantAdmin.setAuthority(Authority.TENANT_ADMIN); | ||
69 | + tenantAdmin.setTenantId(savedTenant.getId()); | ||
70 | + tenantAdmin.setEmail("tenant2@thingsboard.org"); | ||
71 | + tenantAdmin.setFirstName("Joe"); | ||
72 | + tenantAdmin.setLastName("Downs"); | ||
73 | + | ||
74 | + tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); | ||
75 | + | ||
76 | + wsClient = buildAndConnectWebSocketClient(); | ||
77 | + } | ||
78 | + | ||
79 | + @After | ||
80 | + public void afterTest() throws Exception { | ||
81 | + wsClient.close(); | ||
82 | + | ||
83 | + loginSysAdmin(); | ||
84 | + | ||
85 | + doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) | ||
86 | + .andExpect(status().isOk()); | ||
87 | + } | ||
88 | + | ||
89 | + @Test | ||
90 | + public void testEntityDataHistoryWsCmd() throws Exception { | ||
91 | + Device device = new Device(); | ||
92 | + device.setName("Device"); | ||
93 | + device.setType("default"); | ||
94 | + device.setLabel("testLabel" + (int) (Math.random() * 1000)); | ||
95 | + device = doPost("/api/device", device, Device.class); | ||
96 | + | ||
97 | + long now = System.currentTimeMillis(); | ||
98 | + | ||
99 | + DeviceTypeFilter dtf = new DeviceTypeFilter(); | ||
100 | + dtf.setDeviceNameFilter("D"); | ||
101 | + dtf.setDeviceType("default"); | ||
102 | + EntityDataQuery edq = new EntityDataQuery(dtf, new EntityDataPageLink(1, 0, null, null), Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); | ||
103 | + | ||
104 | + EntityHistoryCmd historyCmd = new EntityHistoryCmd(); | ||
105 | + historyCmd.setKeys(Arrays.asList("temperature")); | ||
106 | + historyCmd.setAgg(Aggregation.NONE); | ||
107 | + historyCmd.setLimit(1000); | ||
108 | + historyCmd.setStartTs(now - TimeUnit.HOURS.toMillis(1)); | ||
109 | + historyCmd.setEndTs(now); | ||
110 | + EntityDataCmd cmd = new EntityDataCmd(1, edq, historyCmd, null, null); | ||
111 | + | ||
112 | + TelemetryPluginCmdsWrapper wrapper = new TelemetryPluginCmdsWrapper(); | ||
113 | + wrapper.setEntityDataCmds(Collections.singletonList(cmd)); | ||
114 | + | ||
115 | + wsClient.send(mapper.writeValueAsString(wrapper)); | ||
116 | + String msg = wsClient.waitForReply(); | ||
117 | + EntityDataUpdate update = mapper.readValue(msg, EntityDataUpdate.class); | ||
118 | + Assert.assertEquals(1, update.getCmdId()); | ||
119 | + PageData<EntityData> pageData = update.getData(); | ||
120 | + Assert.assertNotNull(pageData); | ||
121 | + Assert.assertEquals(1, pageData.getData().size()); | ||
122 | + Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId()); | ||
123 | + Assert.assertEquals(0, pageData.getData().get(0).getTimeseries().get("temperature").length); | ||
124 | + | ||
125 | + TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L)); | ||
126 | + TsKvEntry dataPoint2 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(2), new LongDataEntry("temperature", 42L)); | ||
127 | + TsKvEntry dataPoint3 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(3), new LongDataEntry("temperature", 42L)); | ||
128 | + tsService.save(device.getTenantId(), device.getId(), Arrays.asList(dataPoint1, dataPoint2, dataPoint3), 0).get(); | ||
129 | + | ||
130 | + wsClient.send(mapper.writeValueAsString(wrapper)); | ||
131 | + msg = wsClient.waitForReply(); | ||
132 | + update = mapper.readValue(msg, EntityDataUpdate.class); | ||
133 | + Assert.assertEquals(1, update.getCmdId()); | ||
134 | + pageData = update.getData(); | ||
135 | + Assert.assertNotNull(pageData); | ||
136 | + Assert.assertEquals(1, pageData.getData().size()); | ||
137 | + Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId()); | ||
138 | + TsValue[] tsArray = pageData.getData().get(0).getTimeseries().get("temperature"); | ||
139 | + Assert.assertEquals(3, tsArray.length); | ||
140 | + Assert.assertEquals(new TsValue(dataPoint1.getTs(), dataPoint1.getValueAsString()), tsArray[0]); | ||
141 | + Assert.assertEquals(new TsValue(dataPoint2.getTs(), dataPoint2.getValueAsString()), tsArray[1]); | ||
142 | + Assert.assertEquals(new TsValue(dataPoint3.getTs(), dataPoint3.getValueAsString()), tsArray[2]); | ||
143 | + } | ||
144 | + | ||
145 | +} |
@@ -26,6 +26,8 @@ import java.util.Arrays; | @@ -26,6 +26,8 @@ import java.util.Arrays; | ||
26 | 26 | ||
27 | @RunWith(ClasspathSuite.class) | 27 | @RunWith(ClasspathSuite.class) |
28 | @ClasspathSuite.ClassnameFilters({ | 28 | @ClasspathSuite.ClassnameFilters({ |
29 | +// "org.thingsboard.server.controller.sql.WebsocketApiSqlTest", | ||
30 | +// "org.thingsboard.server.controller.sql.EntityQueryControllerSqlTest", | ||
29 | "org.thingsboard.server.controller.sql.*Test", | 31 | "org.thingsboard.server.controller.sql.*Test", |
30 | }) | 32 | }) |
31 | public class ControllerSqlTestSuite { | 33 | public class ControllerSqlTestSuite { |
1 | +/** | ||
2 | + * Copyright © 2016-2020 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.controller; | ||
17 | + | ||
18 | +import lombok.extern.slf4j.Slf4j; | ||
19 | +import org.java_websocket.client.WebSocketClient; | ||
20 | +import org.java_websocket.handshake.ServerHandshake; | ||
21 | + | ||
22 | +import java.net.URI; | ||
23 | +import java.nio.channels.NotYetConnectedException; | ||
24 | +import java.util.concurrent.CountDownLatch; | ||
25 | +import java.util.concurrent.TimeUnit; | ||
26 | + | ||
27 | +@Slf4j | ||
28 | +public class TbTestWebSocketClient extends WebSocketClient { | ||
29 | + | ||
30 | + private volatile String lastMsg; | ||
31 | + private volatile boolean replyReceived; | ||
32 | + private CountDownLatch reply; | ||
33 | + | ||
34 | + public TbTestWebSocketClient(URI serverUri) { | ||
35 | + super(serverUri); | ||
36 | + } | ||
37 | + | ||
38 | + @Override | ||
39 | + public void onOpen(ServerHandshake serverHandshake) { | ||
40 | + | ||
41 | + } | ||
42 | + | ||
43 | + @Override | ||
44 | + public void onMessage(String s) { | ||
45 | + if (!replyReceived) { | ||
46 | + replyReceived = true; | ||
47 | + lastMsg = s; | ||
48 | + if (reply != null) { | ||
49 | + reply.countDown(); | ||
50 | + } | ||
51 | + } | ||
52 | + } | ||
53 | + | ||
54 | + @Override | ||
55 | + public void onClose(int i, String s, boolean b) { | ||
56 | + | ||
57 | + } | ||
58 | + | ||
59 | + @Override | ||
60 | + public void onError(Exception e) { | ||
61 | + | ||
62 | + } | ||
63 | + | ||
64 | + @Override | ||
65 | + public void send(String text) throws NotYetConnectedException { | ||
66 | + reply = new CountDownLatch(1); | ||
67 | + replyReceived = false; | ||
68 | + super.send(text); | ||
69 | + } | ||
70 | + | ||
71 | + public String waitForReply() { | ||
72 | + try { | ||
73 | + reply.await(3, TimeUnit.SECONDS); | ||
74 | + } catch (InterruptedException e) { | ||
75 | + log.warn("Failed to await reply", e); | ||
76 | + } | ||
77 | + return lastMsg; | ||
78 | + } | ||
79 | +} |
1 | +/** | ||
2 | + * Copyright © 2016-2020 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.controller.sql; | ||
17 | + | ||
18 | +import org.thingsboard.server.controller.BaseEntityQueryControllerTest; | ||
19 | +import org.thingsboard.server.controller.BaseWebsocketApiTest; | ||
20 | +import org.thingsboard.server.dao.service.DaoSqlTest; | ||
21 | + | ||
22 | +@DaoSqlTest | ||
23 | +public class WebsocketApiSqlTest extends BaseWebsocketApiTest { | ||
24 | +} |
@@ -37,7 +37,6 @@ | @@ -37,7 +37,6 @@ | ||
37 | <blackBoxTests.skip>true</blackBoxTests.skip> | 37 | <blackBoxTests.skip>true</blackBoxTests.skip> |
38 | <testcontainers.version>1.9.1</testcontainers.version> | 38 | <testcontainers.version>1.9.1</testcontainers.version> |
39 | <zeroturnaround.version>1.10</zeroturnaround.version> | 39 | <zeroturnaround.version>1.10</zeroturnaround.version> |
40 | - <java-websocket.version>1.3.9</java-websocket.version> | ||
41 | <httpclient.version>4.5.6</httpclient.version> | 40 | <httpclient.version>4.5.6</httpclient.version> |
42 | </properties> | 41 | </properties> |
43 | 42 | ||
@@ -55,7 +54,6 @@ | @@ -55,7 +54,6 @@ | ||
55 | <dependency> | 54 | <dependency> |
56 | <groupId>org.java-websocket</groupId> | 55 | <groupId>org.java-websocket</groupId> |
57 | <artifactId>Java-WebSocket</artifactId> | 56 | <artifactId>Java-WebSocket</artifactId> |
58 | - <version>${java-websocket.version}</version> | ||
59 | </dependency> | 57 | </dependency> |
60 | <dependency> | 58 | <dependency> |
61 | <groupId>org.apache.httpcomponents</groupId> | 59 | <groupId>org.apache.httpcomponents</groupId> |
@@ -105,6 +105,7 @@ | @@ -105,6 +105,7 @@ | ||
105 | <ua-parser.version>1.4.3</ua-parser.version> | 105 | <ua-parser.version>1.4.3</ua-parser.version> |
106 | <commons-beanutils.version>1.9.4</commons-beanutils.version> | 106 | <commons-beanutils.version>1.9.4</commons-beanutils.version> |
107 | <commons-collections.version>3.2.2</commons-collections.version> | 107 | <commons-collections.version>3.2.2</commons-collections.version> |
108 | + <java-websocket.version>1.3.9</java-websocket.version> | ||
108 | </properties> | 109 | </properties> |
109 | 110 | ||
110 | <modules> | 111 | <modules> |
@@ -1346,6 +1347,12 @@ | @@ -1346,6 +1347,12 @@ | ||
1346 | <artifactId>commons-collections</artifactId> | 1347 | <artifactId>commons-collections</artifactId> |
1347 | <version>${commons-collections.version}</version> | 1348 | <version>${commons-collections.version}</version> |
1348 | </dependency> | 1349 | </dependency> |
1350 | + <dependency> | ||
1351 | + <groupId>org.java-websocket</groupId> | ||
1352 | + <artifactId>Java-WebSocket</artifactId> | ||
1353 | + <version>${java-websocket.version}</version> | ||
1354 | + <scope>test</scope> | ||
1355 | + </dependency> | ||
1349 | </dependencies> | 1356 | </dependencies> |
1350 | </dependencyManagement> | 1357 | </dependencyManagement> |
1351 | 1358 |