Showing
16 changed files
with
1339 additions
and
424 deletions
... | ... | @@ -304,6 +304,11 @@ |
304 | 304 | <groupId>com.github.ua-parser</groupId> |
305 | 305 | <artifactId>uap-java</artifactId> |
306 | 306 | </dependency> |
307 | + <dependency> | |
308 | + <groupId>org.java-websocket</groupId> | |
309 | + <artifactId>Java-WebSocket</artifactId> | |
310 | + <scope>test</scope> | |
311 | + </dependency> | |
307 | 312 | </dependencies> |
308 | 313 | |
309 | 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 | 49 | import org.thingsboard.server.service.security.ValidationCallback; |
50 | 50 | import org.thingsboard.server.service.security.ValidationResult; |
51 | 51 | import org.thingsboard.server.service.security.ValidationResultCode; |
52 | +import org.thingsboard.server.service.security.model.SecurityUser; | |
52 | 53 | import org.thingsboard.server.service.security.model.UserPrincipal; |
53 | 54 | import org.thingsboard.server.service.security.permission.Operation; |
55 | +import org.thingsboard.server.service.subscription.TbEntityDataSubscriptionService; | |
54 | 56 | import org.thingsboard.server.service.subscription.TbLocalSubscriptionService; |
55 | 57 | import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope; |
56 | 58 | import org.thingsboard.server.service.subscription.TbAttributeSubscription; |
... | ... | @@ -61,6 +63,9 @@ import org.thingsboard.server.service.telemetry.cmd.v1.SubscriptionCmd; |
61 | 63 | import org.thingsboard.server.service.telemetry.cmd.v1.TelemetryPluginCmd; |
62 | 64 | import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper; |
63 | 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 | 69 | import org.thingsboard.server.service.telemetry.exception.UnauthorizedException; |
65 | 70 | import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; |
66 | 71 | import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate; |
... | ... | @@ -104,7 +109,10 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi |
104 | 109 | private final ConcurrentMap<String, WsSessionMetaData> wsSessionsMap = new ConcurrentHashMap<>(); |
105 | 110 | |
106 | 111 | @Autowired |
107 | - private TbLocalSubscriptionService subService; | |
112 | + private TbLocalSubscriptionService oldSubService; | |
113 | + | |
114 | + @Autowired | |
115 | + private TbEntityDataSubscriptionService entityDataSubService; | |
108 | 116 | |
109 | 117 | @Autowired |
110 | 118 | private TelemetryWebSocketMsgEndpoint msgEndpoint; |
... | ... | @@ -164,7 +172,8 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi |
164 | 172 | break; |
165 | 173 | case CLOSED: |
166 | 174 | wsSessionsMap.remove(sessionId); |
167 | - subService.cancelAllSessionSubscriptions(sessionId); | |
175 | + oldSubService.cancelAllSessionSubscriptions(sessionId); | |
176 | + entityDataSubService.cancelAllSessionSubscriptions(sessionId); | |
168 | 177 | processSessionClose(sessionRef); |
169 | 178 | break; |
170 | 179 | } |
... | ... | @@ -196,6 +205,12 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi |
196 | 205 | if (cmdsWrapper.getHistoryCmds() != null) { |
197 | 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 | 215 | } catch (IOException e) { |
201 | 216 | log.warn("Failed to decode subscription cmd: {}", e.getMessage(), e); |
... | ... | @@ -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 | 241 | @Override |
208 | 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 | 252 | WsSessionMetaData md = wsSessionsMap.get(sessionId); |
210 | 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 | 399 | .allKeys(false) |
357 | 400 | .keyStates(subState) |
358 | 401 | .scope(scope).build(); |
359 | - subService.addSubscription(sub); | |
402 | + oldSubService.addSubscription(sub); | |
360 | 403 | } |
361 | 404 | |
362 | 405 | @Override |
... | ... | @@ -453,7 +496,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi |
453 | 496 | .allKeys(true) |
454 | 497 | .keyStates(subState) |
455 | 498 | .scope(scope).build(); |
456 | - subService.addSubscription(sub); | |
499 | + oldSubService.addSubscription(sub); | |
457 | 500 | } |
458 | 501 | |
459 | 502 | @Override |
... | ... | @@ -534,7 +577,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi |
534 | 577 | .entityId(entityId) |
535 | 578 | .allKeys(true) |
536 | 579 | .keyStates(subState).build(); |
537 | - subService.addSubscription(sub); | |
580 | + oldSubService.addSubscription(sub); | |
538 | 581 | } |
539 | 582 | |
540 | 583 | @Override |
... | ... | @@ -571,7 +614,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi |
571 | 614 | .entityId(entityId) |
572 | 615 | .allKeys(false) |
573 | 616 | .keyStates(subState).build(); |
574 | - subService.addSubscription(sub); | |
617 | + oldSubService.addSubscription(sub); | |
575 | 618 | } |
576 | 619 | |
577 | 620 | @Override |
... | ... | @@ -590,12 +633,32 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi |
590 | 633 | |
591 | 634 | private void unsubscribe(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) { |
592 | 635 | if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) { |
593 | - subService.cancelAllSessionSubscriptions(sessionId); | |
636 | + oldSubService.cancelAllSessionSubscriptions(sessionId); | |
594 | 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 | 662 | private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, SubscriptionCmd cmd) { |
600 | 663 | if (cmd.getEntityId() == null || cmd.getEntityId().isEmpty()) { |
601 | 664 | SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, |
... | ... | @@ -607,10 +670,14 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi |
607 | 670 | } |
608 | 671 | |
609 | 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 | 677 | WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId); |
611 | 678 | if (sessionMD == null) { |
612 | 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 | 681 | SESSION_META_DATA_NOT_FOUND); |
615 | 682 | sendWsMsg(sessionRef, update); |
616 | 683 | return false; |
... | ... | @@ -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 | 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 | 698 | executor.submit(() -> { |
624 | 699 | try { |
625 | - msgEndpoint.send(sessionRef, update.getSubscriptionId(), jsonMapper.writeValueAsString(update)); | |
700 | + msgEndpoint.send(sessionRef, cmdId, jsonMapper.writeValueAsString(update)); | |
626 | 701 | } catch (JsonProcessingException e) { |
627 | 702 | log.warn("[{}] Failed to encode reply: {}", sessionRef.getSessionId(), update, e); |
628 | 703 | } catch (IOException e) { |
... | ... | @@ -631,6 +706,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi |
631 | 706 | }); |
632 | 707 | } |
633 | 708 | |
709 | + | |
634 | 710 | private static Optional<Set<String>> getKeys(TelemetryPluginCmd cmd) { |
635 | 711 | if (!StringUtils.isEmpty(cmd.getKeys())) { |
636 | 712 | Set<String> keys = new HashSet<>(); | ... | ... |
... | ... | @@ -15,6 +15,7 @@ |
15 | 15 | */ |
16 | 16 | package org.thingsboard.server.service.telemetry; |
17 | 17 | |
18 | +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; | |
18 | 19 | import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate; |
19 | 20 | |
20 | 21 | /** |
... | ... | @@ -27,4 +28,7 @@ public interface TelemetryWebSocketService { |
27 | 28 | void handleWebSocketMsg(TelemetryWebSocketSessionRef sessionRef, String msg); |
28 | 29 | |
29 | 30 | void sendWsMsg(String sessionId, SubscriptionUpdate update); |
31 | + | |
32 | + void sendWsMsg(String sessionId, EntityDataUpdate update); | |
33 | + | |
30 | 34 | } | ... | ... |
... | ... | @@ -15,10 +15,12 @@ |
15 | 15 | */ |
16 | 16 | package org.thingsboard.server.service.telemetry.cmd.v2; |
17 | 17 | |
18 | +import lombok.Data; | |
18 | 19 | import org.thingsboard.server.common.data.kv.Aggregation; |
19 | 20 | |
20 | 21 | import java.util.List; |
21 | 22 | |
23 | +@Data | |
22 | 24 | public class EntityHistoryCmd { |
23 | 25 | |
24 | 26 | private List<String> keys; | ... | ... |
... | ... | @@ -91,409 +91,8 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppC |
91 | 91 | @Configuration |
92 | 92 | @ComponentScan({"org.thingsboard.server"}) |
93 | 93 | @WebAppConfiguration |
94 | -@SpringBootTest | |
94 | +@SpringBootTest() | |
95 | 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 | 16 | package org.thingsboard.server.controller; |
17 | 17 | |
18 | 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 | 24 | import org.junit.After; |
20 | 25 | import org.junit.Assert; |
21 | 26 | import org.junit.Before; |
22 | 27 | import org.junit.Test; |
28 | +import org.springframework.boot.web.server.LocalServerPort; | |
23 | 29 | import org.thingsboard.server.common.data.DataConstants; |
24 | 30 | import org.thingsboard.server.common.data.Device; |
25 | 31 | import org.thingsboard.server.common.data.EntityType; |
... | ... | @@ -27,6 +33,7 @@ import org.thingsboard.server.common.data.Tenant; |
27 | 33 | import org.thingsboard.server.common.data.User; |
28 | 34 | import org.thingsboard.server.common.data.id.DeviceId; |
29 | 35 | import org.thingsboard.server.common.data.id.EntityId; |
36 | +import org.thingsboard.server.common.data.kv.Aggregation; | |
30 | 37 | import org.thingsboard.server.common.data.page.PageData; |
31 | 38 | import org.thingsboard.server.common.data.query.DeviceTypeFilter; |
32 | 39 | import org.thingsboard.server.common.data.query.EntityCountQuery; |
... | ... | @@ -40,10 +47,16 @@ import org.thingsboard.server.common.data.query.EntityListFilter; |
40 | 47 | import org.thingsboard.server.common.data.query.KeyFilter; |
41 | 48 | import org.thingsboard.server.common.data.query.NumericFilterPredicate; |
42 | 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 | 55 | import java.util.ArrayList; |
56 | +import java.util.Arrays; | |
45 | 57 | import java.util.Collections; |
46 | 58 | import java.util.List; |
59 | +import java.util.Random; | |
47 | 60 | import java.util.stream.Collectors; |
48 | 61 | |
49 | 62 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
... | ... | @@ -190,23 +203,23 @@ public abstract class BaseEntityQueryControllerTest extends AbstractControllerTe |
190 | 203 | List<Device> devices = new ArrayList<>(); |
191 | 204 | List<Long> temperatures = new ArrayList<>(); |
192 | 205 | List<Long> highTemperatures = new ArrayList<>(); |
193 | - for (int i=0;i<67;i++) { | |
206 | + for (int i = 0; i < 67; i++) { | |
194 | 207 | Device device = new Device(); |
195 | - String name = "Device"+i; | |
208 | + String name = "Device" + i; | |
196 | 209 | device.setName(name); |
197 | 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 | 214 | temperatures.add(temperature); |
202 | 215 | if (temperature > 45) { |
203 | 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 | 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 | 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 | 26 | |
27 | 27 | @RunWith(ClasspathSuite.class) |
28 | 28 | @ClasspathSuite.ClassnameFilters({ |
29 | +// "org.thingsboard.server.controller.sql.WebsocketApiSqlTest", | |
30 | +// "org.thingsboard.server.controller.sql.EntityQueryControllerSqlTest", | |
29 | 31 | "org.thingsboard.server.controller.sql.*Test", |
30 | 32 | }) |
31 | 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 | 37 | <blackBoxTests.skip>true</blackBoxTests.skip> |
38 | 38 | <testcontainers.version>1.9.1</testcontainers.version> |
39 | 39 | <zeroturnaround.version>1.10</zeroturnaround.version> |
40 | - <java-websocket.version>1.3.9</java-websocket.version> | |
41 | 40 | <httpclient.version>4.5.6</httpclient.version> |
42 | 41 | </properties> |
43 | 42 | |
... | ... | @@ -55,7 +54,6 @@ |
55 | 54 | <dependency> |
56 | 55 | <groupId>org.java-websocket</groupId> |
57 | 56 | <artifactId>Java-WebSocket</artifactId> |
58 | - <version>${java-websocket.version}</version> | |
59 | 57 | </dependency> |
60 | 58 | <dependency> |
61 | 59 | <groupId>org.apache.httpcomponents</groupId> | ... | ... |
... | ... | @@ -105,6 +105,7 @@ |
105 | 105 | <ua-parser.version>1.4.3</ua-parser.version> |
106 | 106 | <commons-beanutils.version>1.9.4</commons-beanutils.version> |
107 | 107 | <commons-collections.version>3.2.2</commons-collections.version> |
108 | + <java-websocket.version>1.3.9</java-websocket.version> | |
108 | 109 | </properties> |
109 | 110 | |
110 | 111 | <modules> |
... | ... | @@ -1346,6 +1347,12 @@ |
1346 | 1347 | <artifactId>commons-collections</artifactId> |
1347 | 1348 | <version>${commons-collections.version}</version> |
1348 | 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 | 1356 | </dependencies> |
1350 | 1357 | </dependencyManagement> |
1351 | 1358 | ... | ... |