16 changed files with 1339 additions and 424 deletions
@ -0,0 +1,312 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.subscription; |
|||
|
|||
import com.google.common.util.concurrent.Futures; |
|||
import com.google.common.util.concurrent.ListenableFuture; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.context.annotation.Lazy; |
|||
import org.springframework.context.event.EventListener; |
|||
import org.springframework.stereotype.Service; |
|||
import org.thingsboard.common.util.ThingsBoardThreadFactory; |
|||
import org.thingsboard.server.common.data.EntityView; |
|||
import org.thingsboard.server.common.data.id.CustomerId; |
|||
import org.thingsboard.server.common.data.id.EntityViewId; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; |
|||
import org.thingsboard.server.common.data.kv.ReadTsKvQuery; |
|||
import org.thingsboard.server.common.data.kv.TsKvEntry; |
|||
import org.thingsboard.server.common.data.page.PageData; |
|||
import org.thingsboard.server.common.data.query.EntityData; |
|||
import org.thingsboard.server.common.data.query.EntityDataQuery; |
|||
import org.thingsboard.server.common.data.query.TsValue; |
|||
import org.thingsboard.server.common.msg.queue.ServiceType; |
|||
import org.thingsboard.server.common.msg.queue.TbCallback; |
|||
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; |
|||
import org.thingsboard.server.dao.entity.EntityService; |
|||
import org.thingsboard.server.dao.entityview.EntityViewService; |
|||
import org.thingsboard.server.dao.timeseries.TimeseriesService; |
|||
import org.thingsboard.server.queue.discovery.ClusterTopologyChangeEvent; |
|||
import org.thingsboard.server.queue.discovery.PartitionChangeEvent; |
|||
import org.thingsboard.server.queue.discovery.PartitionService; |
|||
import org.thingsboard.server.queue.util.TbCoreComponent; |
|||
import org.thingsboard.server.service.queue.TbClusterService; |
|||
import org.thingsboard.server.service.telemetry.TelemetryWebSocketService; |
|||
import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; |
|||
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; |
|||
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd; |
|||
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; |
|||
import org.thingsboard.server.service.telemetry.cmd.v2.EntityHistoryCmd; |
|||
import org.thingsboard.server.service.telemetry.cmd.v2.LatestValueCmd; |
|||
import org.thingsboard.server.service.telemetry.cmd.v2.TimeSeriesCmd; |
|||
import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate; |
|||
|
|||
import javax.annotation.PostConstruct; |
|||
import javax.annotation.PreDestroy; |
|||
import java.util.ArrayList; |
|||
import java.util.HashMap; |
|||
import java.util.LinkedHashMap; |
|||
import java.util.List; |
|||
import java.util.Map; |
|||
import java.util.Set; |
|||
import java.util.concurrent.ConcurrentHashMap; |
|||
import java.util.concurrent.ExecutionException; |
|||
import java.util.concurrent.ExecutorService; |
|||
import java.util.concurrent.Executors; |
|||
import java.util.function.Function; |
|||
import java.util.stream.Collectors; |
|||
|
|||
@Slf4j |
|||
@TbCoreComponent |
|||
@Service |
|||
public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubscriptionService { |
|||
|
|||
private static final int DEFAULT_LIMIT = 100; |
|||
private final Set<TopicPartitionInfo> currentPartitions = ConcurrentHashMap.newKeySet(); |
|||
private final Map<String, Map<Integer, TbSubscription>> subscriptionsBySessionId = new ConcurrentHashMap<>(); |
|||
|
|||
@Autowired |
|||
private TelemetryWebSocketService wsService; |
|||
|
|||
@Autowired |
|||
private EntityViewService entityViewService; |
|||
|
|||
@Autowired |
|||
private EntityService entityService; |
|||
|
|||
@Autowired |
|||
private PartitionService partitionService; |
|||
|
|||
@Autowired |
|||
private TbClusterService clusterService; |
|||
|
|||
@Autowired |
|||
@Lazy |
|||
private SubscriptionManagerService subscriptionManagerService; |
|||
|
|||
@Autowired |
|||
private TimeseriesService tsService; |
|||
|
|||
private ExecutorService wsCallBackExecutor; |
|||
|
|||
@PostConstruct |
|||
public void initExecutor() { |
|||
wsCallBackExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("ws-entity-sub-callback")); |
|||
} |
|||
|
|||
@PreDestroy |
|||
public void shutdownExecutor() { |
|||
if (wsCallBackExecutor != null) { |
|||
wsCallBackExecutor.shutdownNow(); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
@EventListener(PartitionChangeEvent.class) |
|||
public void onApplicationEvent(PartitionChangeEvent partitionChangeEvent) { |
|||
if (ServiceType.TB_CORE.equals(partitionChangeEvent.getServiceType())) { |
|||
currentPartitions.clear(); |
|||
currentPartitions.addAll(partitionChangeEvent.getPartitions()); |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
@EventListener(ClusterTopologyChangeEvent.class) |
|||
public void onApplicationEvent(ClusterTopologyChangeEvent event) { |
|||
if (event.getServiceQueueKeys().stream().anyMatch(key -> ServiceType.TB_CORE.equals(key.getServiceType()))) { |
|||
/* |
|||
* If the cluster topology has changed, we need to push all current subscriptions to SubscriptionManagerService again. |
|||
* Otherwise, the SubscriptionManagerService may "forget" those subscriptions in case of restart. |
|||
* Although this is resource consuming operation, it is cheaper than sending ping/pong commands periodically |
|||
* 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. |
|||
* Even if we cache locally the list of active subscriptions by entity id, it is still time consuming operation to get them from cache |
|||
* Since number of subscriptions is usually much less then number of devices that are pushing data. |
|||
// */
|
|||
// subscriptionsBySessionId.values().forEach(map -> map.values()
|
|||
// .forEach(sub -> pushSubscriptionToManagerService(sub, false)));
|
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void handleCmd(TelemetryWebSocketSessionRef session, EntityDataCmd cmd) { |
|||
if (cmd.getHistoryCmd() != null) { |
|||
handleHistoryCmd(session, cmd.getCmdId(), cmd.getQuery(), cmd.getHistoryCmd()); |
|||
} else if (cmd.getLatestCmd() != null) { |
|||
handleLatestCmd(session, cmd.getCmdId(), cmd.getQuery(), cmd.getLatestCmd()); |
|||
} else { |
|||
handleTimeseriesCmd(session, cmd.getCmdId(), cmd.getQuery(), cmd.getTsCmd()); |
|||
} |
|||
} |
|||
|
|||
private void handleTimeseriesCmd(TelemetryWebSocketSessionRef session, int cmdId, EntityDataQuery query, TimeSeriesCmd tsCmd) { |
|||
} |
|||
|
|||
private void handleLatestCmd(TelemetryWebSocketSessionRef session, int cmdId, EntityDataQuery query, LatestValueCmd latestCmd) { |
|||
|
|||
} |
|||
|
|||
private void handleHistoryCmd(TelemetryWebSocketSessionRef session, int cmdId, EntityDataQuery query, EntityHistoryCmd historyCmd) { |
|||
TenantId tenantId = session.getSecurityCtx().getTenantId(); |
|||
CustomerId customerId = session.getSecurityCtx().getCustomerId(); |
|||
PageData<EntityData> data = entityService.findEntityDataByQuery(tenantId, customerId, query); |
|||
List<ReadTsKvQuery> tsKvQueryList = historyCmd.getKeys().stream().map(key -> new BaseReadTsKvQuery( |
|||
key, historyCmd.getStartTs(), historyCmd.getEndTs(), historyCmd.getInterval(), getLimit(historyCmd.getLimit()), historyCmd.getAgg() |
|||
)).collect(Collectors.toList()); |
|||
Map<EntityData, ListenableFuture<List<TsKvEntry>>> fetchResultMap = new HashMap<>(); |
|||
data.getData().forEach(entityData -> fetchResultMap.put(entityData, |
|||
tsService.findAll(tenantId, entityData.getEntityId(), tsKvQueryList))); |
|||
Futures.allAsList(fetchResultMap.values()).addListener(() -> { |
|||
fetchResultMap.forEach((entityData, future) -> { |
|||
Map<String, List<TsValue>> keyData = new LinkedHashMap<>(); |
|||
historyCmd.getKeys().forEach(key -> keyData.put(key, new ArrayList<>())); |
|||
try { |
|||
List<TsKvEntry> entityTsData = future.get(); |
|||
if (entityTsData != null) { |
|||
entityTsData.forEach(entry -> keyData.get(entry.getKey()).add(new TsValue(entry.getTs(), entry.getValueAsString()))); |
|||
} |
|||
keyData.forEach((k, v) -> entityData.getTimeseries().put(k, v.toArray(new TsValue[v.size()]))); |
|||
} catch (InterruptedException | ExecutionException e) { |
|||
log.warn("[{}][{}][{}] Failed to fetch historical data", session.getSessionId(), cmdId, entityData.getEntityId(), e); |
|||
} |
|||
}); |
|||
EntityDataUpdate update = new EntityDataUpdate(cmdId, data, null); |
|||
wsService.sendWsMsg(session.getSessionId(), update); |
|||
}, wsCallBackExecutor); |
|||
} |
|||
|
|||
|
|||
@Override |
|||
public void cancelSubscription(String sessionId, EntityDataUnsubscribeCmd subscriptionId) { |
|||
|
|||
} |
|||
|
|||
// //TODO 3.1: replace null callbacks with callbacks from websocket service.
|
|||
// @Override
|
|||
// public void addSubscription(TbSubscription subscription) {
|
|||
// EntityId entityId = subscription.getEntityId();
|
|||
// // Telemetry subscription on Entity Views are handled differently, because we need to allow only certain keys and time ranges;
|
|||
// if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW) && TbSubscriptionType.TIMESERIES.equals(subscription.getType())) {
|
|||
// subscription = resolveEntityViewSubscription((TbTimeseriesSubscription) subscription);
|
|||
// }
|
|||
// pushSubscriptionToManagerService(subscription, true);
|
|||
// registerSubscription(subscription);
|
|||
// }
|
|||
|
|||
// private void pushSubscriptionToManagerService(TbSubscription subscription, boolean pushToLocalService) {
|
|||
// TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, subscription.getTenantId(), subscription.getEntityId());
|
|||
// if (currentPartitions.contains(tpi)) {
|
|||
// // Subscription is managed on the same server;
|
|||
// if (pushToLocalService) {
|
|||
// subscriptionManagerService.addSubscription(subscription, TbCallback.EMPTY);
|
|||
// }
|
|||
// } else {
|
|||
// // Push to the queue;
|
|||
// TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toNewSubscriptionProto(subscription);
|
|||
// clusterService.pushMsgToCore(tpi, subscription.getEntityId().getId(), toCoreMsg, null);
|
|||
// }
|
|||
// }
|
|||
|
|||
@Override |
|||
public void onSubscriptionUpdate(String sessionId, SubscriptionUpdate update, TbCallback callback) { |
|||
// TbSubscription subscription = subscriptionsBySessionId
|
|||
// .getOrDefault(sessionId, Collections.emptyMap()).get(update.getSubscriptionId());
|
|||
// if (subscription != null) {
|
|||
// switch (subscription.getType()) {
|
|||
// case TIMESERIES:
|
|||
// TbTimeseriesSubscription tsSub = (TbTimeseriesSubscription) subscription;
|
|||
// update.getLatestValues().forEach((key, value) -> tsSub.getKeyStates().put(key, value));
|
|||
// break;
|
|||
// case ATTRIBUTES:
|
|||
// TbAttributeSubscription attrSub = (TbAttributeSubscription) subscription;
|
|||
// update.getLatestValues().forEach((key, value) -> attrSub.getKeyStates().put(key, value));
|
|||
// break;
|
|||
// }
|
|||
// wsService.sendWsMsg(sessionId, update);
|
|||
// }
|
|||
// callback.onSuccess();
|
|||
} |
|||
|
|||
// @Override
|
|||
// public void cancelSubscription(String sessionId, int subscriptionId) {
|
|||
// log.debug("[{}][{}] Going to remove subscription.", sessionId, subscriptionId);
|
|||
// Map<Integer, TbSubscription> sessionSubscriptions = subscriptionsBySessionId.get(sessionId);
|
|||
// if (sessionSubscriptions != null) {
|
|||
// TbSubscription subscription = sessionSubscriptions.remove(subscriptionId);
|
|||
// if (subscription != null) {
|
|||
// if (sessionSubscriptions.isEmpty()) {
|
|||
// subscriptionsBySessionId.remove(sessionId);
|
|||
// }
|
|||
// TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, subscription.getTenantId(), subscription.getEntityId());
|
|||
// if (currentPartitions.contains(tpi)) {
|
|||
// // Subscription is managed on the same server;
|
|||
// subscriptionManagerService.cancelSubscription(sessionId, subscriptionId, TbCallback.EMPTY);
|
|||
// } else {
|
|||
// // Push to the queue;
|
|||
// TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toCloseSubscriptionProto(subscription);
|
|||
// clusterService.pushMsgToCore(tpi, subscription.getEntityId().getId(), toCoreMsg, null);
|
|||
// }
|
|||
// } else {
|
|||
// log.debug("[{}][{}] Subscription not found!", sessionId, subscriptionId);
|
|||
// }
|
|||
// } else {
|
|||
// log.debug("[{}] No session subscriptions found!", sessionId);
|
|||
// }
|
|||
// }
|
|||
|
|||
@Override |
|||
public void cancelAllSessionSubscriptions(String sessionId) { |
|||
// Map<Integer, TbSubscription> subscriptions = subscriptionsBySessionId.get(sessionId);
|
|||
// if (subscriptions != null) {
|
|||
// Set<Integer> toRemove = new HashSet<>(subscriptions.keySet());
|
|||
// toRemove.forEach(id -> cancelSubscription(sessionId, id));
|
|||
// }
|
|||
} |
|||
|
|||
private TbSubscription resolveEntityViewSubscription(TbTimeseriesSubscription subscription) { |
|||
EntityView entityView = entityViewService.findEntityViewById(TenantId.SYS_TENANT_ID, new EntityViewId(subscription.getEntityId().getId())); |
|||
|
|||
Map<String, Long> keyStates; |
|||
if (subscription.isAllKeys()) { |
|||
keyStates = entityView.getKeys().getTimeseries().stream().collect(Collectors.toMap(k -> k, k -> 0L)); |
|||
} else { |
|||
keyStates = subscription.getKeyStates().entrySet() |
|||
.stream().filter(entry -> entityView.getKeys().getTimeseries().contains(entry.getKey())) |
|||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); |
|||
} |
|||
|
|||
return TbTimeseriesSubscription.builder() |
|||
.serviceId(subscription.getServiceId()) |
|||
.sessionId(subscription.getSessionId()) |
|||
.subscriptionId(subscription.getSubscriptionId()) |
|||
.tenantId(subscription.getTenantId()) |
|||
.entityId(entityView.getEntityId()) |
|||
.startTime(entityView.getStartTimeMs()) |
|||
.endTime(entityView.getEndTimeMs()) |
|||
.allKeys(false) |
|||
.keyStates(keyStates).build(); |
|||
} |
|||
|
|||
private void registerSubscription(TbSubscription subscription) { |
|||
Map<Integer, TbSubscription> sessionSubscriptions = subscriptionsBySessionId.computeIfAbsent(subscription.getSessionId(), k -> new ConcurrentHashMap<>()); |
|||
sessionSubscriptions.put(subscription.getSubscriptionId(), subscription); |
|||
} |
|||
|
|||
private int getLimit(int limit) { |
|||
return limit == 0 ? DEFAULT_LIMIT : limit; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.service.subscription; |
|||
|
|||
import org.thingsboard.server.common.msg.queue.TbCallback; |
|||
import org.thingsboard.server.queue.discovery.ClusterTopologyChangeEvent; |
|||
import org.thingsboard.server.queue.discovery.PartitionChangeEvent; |
|||
import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; |
|||
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; |
|||
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd; |
|||
import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate; |
|||
|
|||
public interface TbEntityDataSubscriptionService { |
|||
|
|||
void handleCmd(TelemetryWebSocketSessionRef sessionId, EntityDataCmd cmd); |
|||
|
|||
void cancelSubscription(String sessionId, EntityDataUnsubscribeCmd subscriptionId); |
|||
|
|||
void cancelAllSessionSubscriptions(String sessionId); |
|||
|
|||
void onSubscriptionUpdate(String sessionId, SubscriptionUpdate update, TbCallback callback); |
|||
|
|||
void onApplicationEvent(PartitionChangeEvent event); |
|||
|
|||
void onApplicationEvent(ClusterTopologyChangeEvent event); |
|||
} |
|||
@ -0,0 +1,495 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.controller; |
|||
|
|||
import com.fasterxml.jackson.core.type.TypeReference; |
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import com.fasterxml.jackson.databind.ObjectMapper; |
|||
import io.jsonwebtoken.Claims; |
|||
import io.jsonwebtoken.Header; |
|||
import io.jsonwebtoken.Jwt; |
|||
import io.jsonwebtoken.Jwts; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.apache.commons.lang3.StringUtils; |
|||
import org.hamcrest.Matcher; |
|||
import org.junit.After; |
|||
import org.junit.Assert; |
|||
import org.junit.Before; |
|||
import org.junit.Rule; |
|||
import org.junit.rules.TestRule; |
|||
import org.junit.rules.TestWatcher; |
|||
import org.junit.runner.Description; |
|||
import org.junit.runner.RunWith; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.boot.test.context.SpringBootContextLoader; |
|||
import org.springframework.boot.test.context.SpringBootTest; |
|||
import org.springframework.context.annotation.ComponentScan; |
|||
import org.springframework.context.annotation.Configuration; |
|||
import org.springframework.http.HttpHeaders; |
|||
import org.springframework.http.MediaType; |
|||
import org.springframework.http.converter.HttpMessageConverter; |
|||
import org.springframework.http.converter.StringHttpMessageConverter; |
|||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; |
|||
import org.springframework.mock.http.MockHttpInputMessage; |
|||
import org.springframework.mock.http.MockHttpOutputMessage; |
|||
import org.springframework.test.annotation.DirtiesContext; |
|||
import org.springframework.test.context.ActiveProfiles; |
|||
import org.springframework.test.context.ContextConfiguration; |
|||
import org.springframework.test.context.junit4.SpringRunner; |
|||
import org.springframework.test.web.servlet.MockMvc; |
|||
import org.springframework.test.web.servlet.MvcResult; |
|||
import org.springframework.test.web.servlet.ResultActions; |
|||
import org.springframework.test.web.servlet.ResultMatcher; |
|||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; |
|||
import org.springframework.util.LinkedMultiValueMap; |
|||
import org.springframework.util.MultiValueMap; |
|||
import org.springframework.web.context.WebApplicationContext; |
|||
import org.thingsboard.server.common.data.BaseData; |
|||
import org.thingsboard.server.common.data.Customer; |
|||
import org.thingsboard.server.common.data.Tenant; |
|||
import org.thingsboard.server.common.data.User; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.id.UUIDBased; |
|||
import org.thingsboard.server.common.data.page.PageLink; |
|||
import org.thingsboard.server.common.data.page.TimePageLink; |
|||
import org.thingsboard.server.common.data.security.Authority; |
|||
import org.thingsboard.server.config.ThingsboardSecurityConfiguration; |
|||
import org.thingsboard.server.service.mail.TestMailService; |
|||
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest; |
|||
import org.thingsboard.server.service.security.auth.rest.LoginRequest; |
|||
|
|||
import java.io.IOException; |
|||
import java.util.ArrayList; |
|||
import java.util.Arrays; |
|||
import java.util.Comparator; |
|||
import java.util.List; |
|||
|
|||
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; |
|||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; |
|||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; |
|||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; |
|||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; |
|||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; |
|||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; |
|||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; |
|||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
|||
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; |
|||
|
|||
@Slf4j |
|||
public abstract class AbstractWebTest { |
|||
|
|||
protected ObjectMapper mapper = new ObjectMapper(); |
|||
|
|||
protected static final String TEST_TENANT_NAME = "TEST TENANT"; |
|||
|
|||
protected static final String SYS_ADMIN_EMAIL = "sysadmin@thingsboard.org"; |
|||
private static final String SYS_ADMIN_PASSWORD = "sysadmin"; |
|||
|
|||
protected static final String TENANT_ADMIN_EMAIL = "testtenant@thingsboard.org"; |
|||
private static final String TENANT_ADMIN_PASSWORD = "tenant"; |
|||
|
|||
protected static final String CUSTOMER_USER_EMAIL = "testcustomer@thingsboard.org"; |
|||
private static final String CUSTOMER_USER_PASSWORD = "customer"; |
|||
|
|||
/** See {@link org.springframework.test.web.servlet.DefaultMvcResult#getAsyncResult(long)} |
|||
* and {@link org.springframework.mock.web.MockAsyncContext#getTimeout()} |
|||
*/ |
|||
private static final long DEFAULT_TIMEOUT = -1L; |
|||
|
|||
protected MediaType contentType = MediaType.APPLICATION_JSON; |
|||
|
|||
protected MockMvc mockMvc; |
|||
|
|||
protected String token; |
|||
protected String refreshToken; |
|||
protected String username; |
|||
|
|||
private TenantId tenantId; |
|||
|
|||
@SuppressWarnings("rawtypes") |
|||
private HttpMessageConverter mappingJackson2HttpMessageConverter; |
|||
|
|||
@SuppressWarnings("rawtypes") |
|||
private HttpMessageConverter stringHttpMessageConverter; |
|||
|
|||
@Autowired |
|||
private WebApplicationContext webApplicationContext; |
|||
|
|||
@Rule |
|||
public TestRule watcher = new TestWatcher() { |
|||
protected void starting(Description description) { |
|||
log.info("Starting test: {}", description.getMethodName()); |
|||
} |
|||
|
|||
protected void finished(Description description) { |
|||
log.info("Finished test: {}", description.getMethodName()); |
|||
} |
|||
}; |
|||
|
|||
@Autowired |
|||
void setConverters(HttpMessageConverter<?>[] converters) { |
|||
|
|||
this.mappingJackson2HttpMessageConverter = Arrays.stream(converters) |
|||
.filter(hmc -> hmc instanceof MappingJackson2HttpMessageConverter) |
|||
.findAny() |
|||
.get(); |
|||
|
|||
this.stringHttpMessageConverter = Arrays.stream(converters) |
|||
.filter(hmc -> hmc instanceof StringHttpMessageConverter) |
|||
.findAny() |
|||
.get(); |
|||
|
|||
Assert.assertNotNull("the JSON message converter must not be null", |
|||
this.mappingJackson2HttpMessageConverter); |
|||
} |
|||
|
|||
@Before |
|||
public void setup() throws Exception { |
|||
log.info("Executing setup"); |
|||
if (this.mockMvc == null) { |
|||
this.mockMvc = webAppContextSetup(webApplicationContext) |
|||
.apply(springSecurity()).build(); |
|||
} |
|||
loginSysAdmin(); |
|||
|
|||
Tenant tenant = new Tenant(); |
|||
tenant.setTitle(TEST_TENANT_NAME); |
|||
Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class); |
|||
Assert.assertNotNull(savedTenant); |
|||
tenantId = savedTenant.getId(); |
|||
|
|||
User tenantAdmin = new User(); |
|||
tenantAdmin.setAuthority(Authority.TENANT_ADMIN); |
|||
tenantAdmin.setTenantId(tenantId); |
|||
tenantAdmin.setEmail(TENANT_ADMIN_EMAIL); |
|||
|
|||
createUserAndLogin(tenantAdmin, TENANT_ADMIN_PASSWORD); |
|||
|
|||
Customer customer = new Customer(); |
|||
customer.setTitle("Customer"); |
|||
customer.setTenantId(tenantId); |
|||
Customer savedCustomer = doPost("/api/customer", customer, Customer.class); |
|||
|
|||
User customerUser = new User(); |
|||
customerUser.setAuthority(Authority.CUSTOMER_USER); |
|||
customerUser.setTenantId(tenantId); |
|||
customerUser.setCustomerId(savedCustomer.getId()); |
|||
customerUser.setEmail(CUSTOMER_USER_EMAIL); |
|||
|
|||
createUserAndLogin(customerUser, CUSTOMER_USER_PASSWORD); |
|||
|
|||
logout(); |
|||
|
|||
log.info("Executed setup"); |
|||
} |
|||
|
|||
@After |
|||
public void teardown() throws Exception { |
|||
log.info("Executing teardown"); |
|||
loginSysAdmin(); |
|||
doDelete("/api/tenant/" + tenantId.getId().toString()) |
|||
.andExpect(status().isOk()); |
|||
log.info("Executed teardown"); |
|||
} |
|||
|
|||
protected void loginSysAdmin() throws Exception { |
|||
login(SYS_ADMIN_EMAIL, SYS_ADMIN_PASSWORD); |
|||
} |
|||
|
|||
protected void loginTenantAdmin() throws Exception { |
|||
login(TENANT_ADMIN_EMAIL, TENANT_ADMIN_PASSWORD); |
|||
} |
|||
|
|||
protected void loginCustomerUser() throws Exception { |
|||
login(CUSTOMER_USER_EMAIL, CUSTOMER_USER_PASSWORD); |
|||
} |
|||
|
|||
private Tenant savedDifferentTenant; |
|||
protected void loginDifferentTenant() throws Exception { |
|||
loginSysAdmin(); |
|||
Tenant tenant = new Tenant(); |
|||
tenant.setTitle("Different tenant"); |
|||
savedDifferentTenant = doPost("/api/tenant", tenant, Tenant.class); |
|||
Assert.assertNotNull(savedDifferentTenant); |
|||
User differentTenantAdmin = new User(); |
|||
differentTenantAdmin.setAuthority(Authority.TENANT_ADMIN); |
|||
differentTenantAdmin.setTenantId(savedDifferentTenant.getId()); |
|||
differentTenantAdmin.setEmail("different_tenant@thingsboard.org"); |
|||
|
|||
createUserAndLogin(differentTenantAdmin, "testPassword"); |
|||
} |
|||
|
|||
protected void deleteDifferentTenant() throws Exception { |
|||
loginSysAdmin(); |
|||
doDelete("/api/tenant/" + savedDifferentTenant.getId().getId().toString()) |
|||
.andExpect(status().isOk()); |
|||
} |
|||
|
|||
protected User createUserAndLogin(User user, String password) throws Exception { |
|||
User savedUser = doPost("/api/user", user, User.class); |
|||
logout(); |
|||
doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken) |
|||
.andExpect(status().isSeeOther()) |
|||
.andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken)); |
|||
JsonNode activateRequest = new ObjectMapper().createObjectNode() |
|||
.put("activateToken", TestMailService.currentActivateToken) |
|||
.put("password", password); |
|||
JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class); |
|||
validateAndSetJwtToken(tokenInfo, user.getEmail()); |
|||
return savedUser; |
|||
} |
|||
|
|||
protected void login(String username, String password) throws Exception { |
|||
this.token = null; |
|||
this.refreshToken = null; |
|||
this.username = null; |
|||
JsonNode tokenInfo = readResponse(doPost("/api/auth/login", new LoginRequest(username, password)).andExpect(status().isOk()), JsonNode.class); |
|||
validateAndSetJwtToken(tokenInfo, username); |
|||
} |
|||
|
|||
protected void refreshToken() throws Exception { |
|||
this.token = null; |
|||
JsonNode tokenInfo = readResponse(doPost("/api/auth/token", new RefreshTokenRequest(this.refreshToken)).andExpect(status().isOk()), JsonNode.class); |
|||
validateAndSetJwtToken(tokenInfo, this.username); |
|||
} |
|||
|
|||
protected void validateAndSetJwtToken(JsonNode tokenInfo, String username) { |
|||
Assert.assertNotNull(tokenInfo); |
|||
Assert.assertTrue(tokenInfo.has("token")); |
|||
Assert.assertTrue(tokenInfo.has("refreshToken")); |
|||
String token = tokenInfo.get("token").asText(); |
|||
String refreshToken = tokenInfo.get("refreshToken").asText(); |
|||
validateJwtToken(token, username); |
|||
validateJwtToken(refreshToken, username); |
|||
this.token = token; |
|||
this.refreshToken = refreshToken; |
|||
this.username = username; |
|||
} |
|||
|
|||
protected void validateJwtToken(String token, String username) { |
|||
Assert.assertNotNull(token); |
|||
Assert.assertFalse(token.isEmpty()); |
|||
int i = token.lastIndexOf('.'); |
|||
Assert.assertTrue(i > 0); |
|||
String withoutSignature = token.substring(0, i + 1); |
|||
Jwt<Header, Claims> jwsClaims = Jwts.parser().parseClaimsJwt(withoutSignature); |
|||
Claims claims = jwsClaims.getBody(); |
|||
String subject = claims.getSubject(); |
|||
Assert.assertEquals(username, subject); |
|||
} |
|||
|
|||
protected void logout() throws Exception { |
|||
this.token = null; |
|||
this.refreshToken = null; |
|||
this.username = null; |
|||
} |
|||
|
|||
protected void setJwtToken(MockHttpServletRequestBuilder request) { |
|||
if (this.token != null) { |
|||
request.header(ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM, "Bearer " + this.token); |
|||
} |
|||
} |
|||
|
|||
protected ResultActions doGet(String urlTemplate, Object... urlVariables) throws Exception { |
|||
MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables); |
|||
setJwtToken(getRequest); |
|||
return mockMvc.perform(getRequest); |
|||
} |
|||
|
|||
protected <T> T doGet(String urlTemplate, Class<T> responseClass, Object... urlVariables) throws Exception { |
|||
return readResponse(doGet(urlTemplate, urlVariables).andExpect(status().isOk()), responseClass); |
|||
} |
|||
|
|||
protected <T> T doGetAsync(String urlTemplate, Class<T> responseClass, Object... urlVariables) throws Exception { |
|||
return readResponse(doGetAsync(urlTemplate, urlVariables).andExpect(status().isOk()), responseClass); |
|||
} |
|||
|
|||
protected ResultActions doGetAsync(String urlTemplate, Object... urlVariables) throws Exception { |
|||
MockHttpServletRequestBuilder getRequest; |
|||
getRequest = get(urlTemplate, urlVariables); |
|||
setJwtToken(getRequest); |
|||
return mockMvc.perform(asyncDispatch(mockMvc.perform(getRequest).andExpect(request().asyncStarted()).andReturn())); |
|||
} |
|||
|
|||
protected <T> T doGetTyped(String urlTemplate, TypeReference<T> responseType, Object... urlVariables) throws Exception { |
|||
return readResponse(doGet(urlTemplate, urlVariables).andExpect(status().isOk()), responseType); |
|||
} |
|||
|
|||
protected <T> T doGetTypedWithPageLink(String urlTemplate, TypeReference<T> responseType, |
|||
PageLink pageLink, |
|||
Object... urlVariables) throws Exception { |
|||
List<Object> pageLinkVariables = new ArrayList<>(); |
|||
urlTemplate += "pageSize={pageSize}&page={page}"; |
|||
pageLinkVariables.add(pageLink.getPageSize()); |
|||
pageLinkVariables.add(pageLink.getPage()); |
|||
if (StringUtils.isNotEmpty(pageLink.getTextSearch())) { |
|||
urlTemplate += "&textSearch={textSearch}"; |
|||
pageLinkVariables.add(pageLink.getTextSearch()); |
|||
} |
|||
if (pageLink.getSortOrder() != null) { |
|||
urlTemplate += "&sortProperty={sortProperty}&sortOrder={sortOrder}"; |
|||
pageLinkVariables.add(pageLink.getSortOrder().getProperty()); |
|||
pageLinkVariables.add(pageLink.getSortOrder().getDirection().name()); |
|||
} |
|||
|
|||
Object[] vars = new Object[urlVariables.length + pageLinkVariables.size()]; |
|||
System.arraycopy(urlVariables, 0, vars, 0, urlVariables.length); |
|||
System.arraycopy(pageLinkVariables.toArray(), 0, vars, urlVariables.length, pageLinkVariables.size()); |
|||
|
|||
return readResponse(doGet(urlTemplate, vars).andExpect(status().isOk()), responseType); |
|||
} |
|||
|
|||
protected <T> T doGetTypedWithTimePageLink(String urlTemplate, TypeReference<T> responseType, |
|||
TimePageLink pageLink, |
|||
Object... urlVariables) throws Exception { |
|||
List<Object> pageLinkVariables = new ArrayList<>(); |
|||
urlTemplate += "pageSize={pageSize}&page={page}"; |
|||
pageLinkVariables.add(pageLink.getPageSize()); |
|||
pageLinkVariables.add(pageLink.getPage()); |
|||
if (pageLink.getStartTime() != null) { |
|||
urlTemplate += "&startTime={startTime}"; |
|||
pageLinkVariables.add(pageLink.getStartTime()); |
|||
} |
|||
if (pageLink.getEndTime() != null) { |
|||
urlTemplate += "&endTime={endTime}"; |
|||
pageLinkVariables.add(pageLink.getEndTime()); |
|||
} |
|||
if (StringUtils.isNotEmpty(pageLink.getTextSearch())) { |
|||
urlTemplate += "&textSearch={textSearch}"; |
|||
pageLinkVariables.add(pageLink.getTextSearch()); |
|||
} |
|||
if (pageLink.getSortOrder() != null) { |
|||
urlTemplate += "&sortProperty={sortProperty}&sortOrder={sortOrder}"; |
|||
pageLinkVariables.add(pageLink.getSortOrder().getProperty()); |
|||
pageLinkVariables.add(pageLink.getSortOrder().getDirection().name()); |
|||
} |
|||
Object[] vars = new Object[urlVariables.length + pageLinkVariables.size()]; |
|||
System.arraycopy(urlVariables, 0, vars, 0, urlVariables.length); |
|||
System.arraycopy(pageLinkVariables.toArray(), 0, vars, urlVariables.length, pageLinkVariables.size()); |
|||
|
|||
return readResponse(doGet(urlTemplate, vars).andExpect(status().isOk()), responseType); |
|||
} |
|||
|
|||
protected <T> T doPost(String urlTemplate, Class<T> responseClass, String... params) throws Exception { |
|||
return readResponse(doPost(urlTemplate, params).andExpect(status().isOk()), responseClass); |
|||
} |
|||
|
|||
protected <T> T doPost(String urlTemplate, T content, Class<T> responseClass, ResultMatcher resultMatcher, String... params) throws Exception { |
|||
return readResponse(doPost(urlTemplate, content, params).andExpect(resultMatcher), responseClass); |
|||
} |
|||
|
|||
protected <T> T doPost(String urlTemplate, T content, Class<T> responseClass, String... params) throws Exception { |
|||
return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseClass); |
|||
} |
|||
|
|||
protected <T,R> R doPostWithResponse(String urlTemplate, T content, Class<R> responseClass, String... params) throws Exception { |
|||
return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseClass); |
|||
} |
|||
|
|||
protected <T,R> R doPostWithTypedResponse(String urlTemplate, T content, TypeReference<R> responseType, String... params) throws Exception { |
|||
return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseType); |
|||
} |
|||
|
|||
protected <T> T doPostAsync(String urlTemplate, T content, Class<T> responseClass, ResultMatcher resultMatcher, String... params) throws Exception { |
|||
return readResponse(doPostAsync(urlTemplate, content, DEFAULT_TIMEOUT, params).andExpect(resultMatcher), responseClass); |
|||
} |
|||
|
|||
protected <T> T doPostAsync(String urlTemplate, T content, Class<T> responseClass, ResultMatcher resultMatcher, Long timeout, String... params) throws Exception { |
|||
return readResponse(doPostAsync(urlTemplate, content, timeout, params).andExpect(resultMatcher), responseClass); |
|||
} |
|||
|
|||
protected <T> T doDelete(String urlTemplate, Class<T> responseClass, String... params) throws Exception { |
|||
return readResponse(doDelete(urlTemplate, params).andExpect(status().isOk()), responseClass); |
|||
} |
|||
|
|||
protected ResultActions doPost(String urlTemplate, String... params) throws Exception { |
|||
MockHttpServletRequestBuilder postRequest = post(urlTemplate); |
|||
setJwtToken(postRequest); |
|||
populateParams(postRequest, params); |
|||
return mockMvc.perform(postRequest); |
|||
} |
|||
|
|||
protected <T> ResultActions doPost(String urlTemplate, T content, String... params) throws Exception { |
|||
MockHttpServletRequestBuilder postRequest = post(urlTemplate); |
|||
setJwtToken(postRequest); |
|||
String json = json(content); |
|||
postRequest.contentType(contentType).content(json); |
|||
return mockMvc.perform(postRequest); |
|||
} |
|||
|
|||
protected <T> ResultActions doPostAsync(String urlTemplate, T content, Long timeout, String... params) throws Exception { |
|||
MockHttpServletRequestBuilder postRequest = post(urlTemplate); |
|||
setJwtToken(postRequest); |
|||
String json = json(content); |
|||
postRequest.contentType(contentType).content(json); |
|||
MvcResult result = mockMvc.perform(postRequest).andReturn(); |
|||
result.getAsyncResult(timeout); |
|||
return mockMvc.perform(asyncDispatch(result)); |
|||
} |
|||
|
|||
protected ResultActions doDelete(String urlTemplate, String... params) throws Exception { |
|||
MockHttpServletRequestBuilder deleteRequest = delete(urlTemplate); |
|||
setJwtToken(deleteRequest); |
|||
populateParams(deleteRequest, params); |
|||
return mockMvc.perform(deleteRequest); |
|||
} |
|||
|
|||
protected void populateParams(MockHttpServletRequestBuilder request, String... params) { |
|||
if (params != null && params.length > 0) { |
|||
Assert.assertEquals(0, params.length % 2); |
|||
MultiValueMap<String, String> paramsMap = new LinkedMultiValueMap<>(); |
|||
for (int i = 0; i < params.length; i += 2) { |
|||
paramsMap.add(params[i], params[i + 1]); |
|||
} |
|||
request.params(paramsMap); |
|||
} |
|||
} |
|||
|
|||
@SuppressWarnings("unchecked") |
|||
protected String json(Object o) throws IOException { |
|||
MockHttpOutputMessage mockHttpOutputMessage = new MockHttpOutputMessage(); |
|||
|
|||
HttpMessageConverter converter = o instanceof String ? stringHttpMessageConverter : mappingJackson2HttpMessageConverter; |
|||
converter.write(o, MediaType.APPLICATION_JSON, mockHttpOutputMessage); |
|||
return mockHttpOutputMessage.getBodyAsString(); |
|||
} |
|||
|
|||
@SuppressWarnings("unchecked") |
|||
protected <T> T readResponse(ResultActions result, Class<T> responseClass) throws Exception { |
|||
byte[] content = result.andReturn().getResponse().getContentAsByteArray(); |
|||
MockHttpInputMessage mockHttpInputMessage = new MockHttpInputMessage(content); |
|||
HttpMessageConverter converter = responseClass.equals(String.class) ? stringHttpMessageConverter : mappingJackson2HttpMessageConverter; |
|||
return (T) converter.read(responseClass, mockHttpInputMessage); |
|||
} |
|||
|
|||
protected <T> T readResponse(ResultActions result, TypeReference<T> type) throws Exception { |
|||
byte[] content = result.andReturn().getResponse().getContentAsByteArray(); |
|||
ObjectMapper mapper = new ObjectMapper(); |
|||
return mapper.readerFor(type).readValue(content); |
|||
} |
|||
|
|||
public class IdComparator<D extends BaseData<? extends UUIDBased>> implements Comparator<D> { |
|||
@Override |
|||
public int compare(D o1, D o2) { |
|||
return o1.getId().getId().compareTo(o2.getId().getId()); |
|||
} |
|||
} |
|||
|
|||
protected static <T> ResultMatcher statusReason(Matcher<T> matcher) { |
|||
return jsonPath("$.message", matcher); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,115 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.controller; |
|||
|
|||
import com.fasterxml.jackson.core.type.TypeReference; |
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import com.fasterxml.jackson.databind.ObjectMapper; |
|||
import io.jsonwebtoken.Claims; |
|||
import io.jsonwebtoken.Header; |
|||
import io.jsonwebtoken.Jwt; |
|||
import io.jsonwebtoken.Jwts; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.apache.commons.lang3.StringUtils; |
|||
import org.hamcrest.Matcher; |
|||
import org.junit.After; |
|||
import org.junit.Assert; |
|||
import org.junit.Before; |
|||
import org.junit.Rule; |
|||
import org.junit.rules.TestRule; |
|||
import org.junit.rules.TestWatcher; |
|||
import org.junit.runner.Description; |
|||
import org.junit.runner.RunWith; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.boot.test.context.SpringBootContextLoader; |
|||
import org.springframework.boot.test.context.SpringBootTest; |
|||
import org.springframework.boot.web.server.LocalServerPort; |
|||
import org.springframework.context.annotation.ComponentScan; |
|||
import org.springframework.context.annotation.Configuration; |
|||
import org.springframework.http.HttpHeaders; |
|||
import org.springframework.http.MediaType; |
|||
import org.springframework.http.converter.HttpMessageConverter; |
|||
import org.springframework.http.converter.StringHttpMessageConverter; |
|||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; |
|||
import org.springframework.mock.http.MockHttpInputMessage; |
|||
import org.springframework.mock.http.MockHttpOutputMessage; |
|||
import org.springframework.test.annotation.DirtiesContext; |
|||
import org.springframework.test.context.ActiveProfiles; |
|||
import org.springframework.test.context.ContextConfiguration; |
|||
import org.springframework.test.context.junit4.SpringRunner; |
|||
import org.springframework.test.web.servlet.MockMvc; |
|||
import org.springframework.test.web.servlet.MvcResult; |
|||
import org.springframework.test.web.servlet.ResultActions; |
|||
import org.springframework.test.web.servlet.ResultMatcher; |
|||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; |
|||
import org.springframework.util.LinkedMultiValueMap; |
|||
import org.springframework.util.MultiValueMap; |
|||
import org.springframework.web.context.WebApplicationContext; |
|||
import org.thingsboard.server.common.data.BaseData; |
|||
import org.thingsboard.server.common.data.Customer; |
|||
import org.thingsboard.server.common.data.Tenant; |
|||
import org.thingsboard.server.common.data.User; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.id.UUIDBased; |
|||
import org.thingsboard.server.common.data.page.PageLink; |
|||
import org.thingsboard.server.common.data.page.TimePageLink; |
|||
import org.thingsboard.server.common.data.security.Authority; |
|||
import org.thingsboard.server.config.ThingsboardSecurityConfiguration; |
|||
import org.thingsboard.server.service.mail.TestMailService; |
|||
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest; |
|||
import org.thingsboard.server.service.security.auth.rest.LoginRequest; |
|||
|
|||
import java.io.IOException; |
|||
import java.net.URI; |
|||
import java.net.URISyntaxException; |
|||
import java.util.ArrayList; |
|||
import java.util.Arrays; |
|||
import java.util.Comparator; |
|||
import java.util.List; |
|||
|
|||
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; |
|||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch; |
|||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; |
|||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; |
|||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; |
|||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; |
|||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; |
|||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request; |
|||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
|||
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; |
|||
|
|||
@ActiveProfiles("test") |
|||
@RunWith(SpringRunner.class) |
|||
@ContextConfiguration(classes = AbstractControllerTest.class, loader = SpringBootContextLoader.class) |
|||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) |
|||
@Configuration |
|||
@ComponentScan({"org.thingsboard.server"}) |
|||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) |
|||
@Slf4j |
|||
public abstract class AbstractWebsocketTest extends AbstractWebTest { |
|||
|
|||
protected static final String WS_URL = "ws://localhost:"; |
|||
|
|||
@LocalServerPort |
|||
protected int wsPort; |
|||
|
|||
protected TbTestWebSocketClient buildAndConnectWebSocketClient() throws URISyntaxException, InterruptedException { |
|||
TbTestWebSocketClient wsClient = new TbTestWebSocketClient(new URI(WS_URL + wsPort + "/api/ws/plugins/telemetry?token=" + token)); |
|||
Assert.assertTrue(wsClient.connectBlocking()); |
|||
return wsClient; |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,145 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.controller; |
|||
|
|||
import org.junit.After; |
|||
import org.junit.Assert; |
|||
import org.junit.Before; |
|||
import org.junit.Test; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.thingsboard.server.common.data.Device; |
|||
import org.thingsboard.server.common.data.Tenant; |
|||
import org.thingsboard.server.common.data.User; |
|||
import org.thingsboard.server.common.data.kv.Aggregation; |
|||
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; |
|||
import org.thingsboard.server.common.data.kv.LongDataEntry; |
|||
import org.thingsboard.server.common.data.kv.TsKvEntry; |
|||
import org.thingsboard.server.common.data.page.PageData; |
|||
import org.thingsboard.server.common.data.query.DeviceTypeFilter; |
|||
import org.thingsboard.server.common.data.query.EntityData; |
|||
import org.thingsboard.server.common.data.query.EntityDataPageLink; |
|||
import org.thingsboard.server.common.data.query.EntityDataQuery; |
|||
import org.thingsboard.server.common.data.query.TsValue; |
|||
import org.thingsboard.server.common.data.security.Authority; |
|||
import org.thingsboard.server.dao.timeseries.TimeseriesService; |
|||
import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper; |
|||
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; |
|||
import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; |
|||
import org.thingsboard.server.service.telemetry.cmd.v2.EntityHistoryCmd; |
|||
|
|||
import java.util.Arrays; |
|||
import java.util.Collections; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
|||
|
|||
public class BaseWebsocketApiTest extends AbstractWebsocketTest { |
|||
|
|||
private Tenant savedTenant; |
|||
private User tenantAdmin; |
|||
private TbTestWebSocketClient wsClient; |
|||
|
|||
@Autowired |
|||
private TimeseriesService tsService; |
|||
|
|||
@Before |
|||
public void beforeTest() throws Exception { |
|||
loginSysAdmin(); |
|||
|
|||
Tenant tenant = new Tenant(); |
|||
tenant.setTitle("My tenant"); |
|||
savedTenant = doPost("/api/tenant", tenant, Tenant.class); |
|||
Assert.assertNotNull(savedTenant); |
|||
|
|||
tenantAdmin = new User(); |
|||
tenantAdmin.setAuthority(Authority.TENANT_ADMIN); |
|||
tenantAdmin.setTenantId(savedTenant.getId()); |
|||
tenantAdmin.setEmail("tenant2@thingsboard.org"); |
|||
tenantAdmin.setFirstName("Joe"); |
|||
tenantAdmin.setLastName("Downs"); |
|||
|
|||
tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1"); |
|||
|
|||
wsClient = buildAndConnectWebSocketClient(); |
|||
} |
|||
|
|||
@After |
|||
public void afterTest() throws Exception { |
|||
wsClient.close(); |
|||
|
|||
loginSysAdmin(); |
|||
|
|||
doDelete("/api/tenant/" + savedTenant.getId().getId().toString()) |
|||
.andExpect(status().isOk()); |
|||
} |
|||
|
|||
@Test |
|||
public void testEntityDataHistoryWsCmd() throws Exception { |
|||
Device device = new Device(); |
|||
device.setName("Device"); |
|||
device.setType("default"); |
|||
device.setLabel("testLabel" + (int) (Math.random() * 1000)); |
|||
device = doPost("/api/device", device, Device.class); |
|||
|
|||
long now = System.currentTimeMillis(); |
|||
|
|||
DeviceTypeFilter dtf = new DeviceTypeFilter(); |
|||
dtf.setDeviceNameFilter("D"); |
|||
dtf.setDeviceType("default"); |
|||
EntityDataQuery edq = new EntityDataQuery(dtf, new EntityDataPageLink(1, 0, null, null), Collections.emptyList(), Collections.emptyList(), Collections.emptyList()); |
|||
|
|||
EntityHistoryCmd historyCmd = new EntityHistoryCmd(); |
|||
historyCmd.setKeys(Arrays.asList("temperature")); |
|||
historyCmd.setAgg(Aggregation.NONE); |
|||
historyCmd.setLimit(1000); |
|||
historyCmd.setStartTs(now - TimeUnit.HOURS.toMillis(1)); |
|||
historyCmd.setEndTs(now); |
|||
EntityDataCmd cmd = new EntityDataCmd(1, edq, historyCmd, null, null); |
|||
|
|||
TelemetryPluginCmdsWrapper wrapper = new TelemetryPluginCmdsWrapper(); |
|||
wrapper.setEntityDataCmds(Collections.singletonList(cmd)); |
|||
|
|||
wsClient.send(mapper.writeValueAsString(wrapper)); |
|||
String msg = wsClient.waitForReply(); |
|||
EntityDataUpdate update = mapper.readValue(msg, EntityDataUpdate.class); |
|||
Assert.assertEquals(1, update.getCmdId()); |
|||
PageData<EntityData> pageData = update.getData(); |
|||
Assert.assertNotNull(pageData); |
|||
Assert.assertEquals(1, pageData.getData().size()); |
|||
Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId()); |
|||
Assert.assertEquals(0, pageData.getData().get(0).getTimeseries().get("temperature").length); |
|||
|
|||
TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L)); |
|||
TsKvEntry dataPoint2 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(2), new LongDataEntry("temperature", 42L)); |
|||
TsKvEntry dataPoint3 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(3), new LongDataEntry("temperature", 42L)); |
|||
tsService.save(device.getTenantId(), device.getId(), Arrays.asList(dataPoint1, dataPoint2, dataPoint3), 0).get(); |
|||
|
|||
wsClient.send(mapper.writeValueAsString(wrapper)); |
|||
msg = wsClient.waitForReply(); |
|||
update = mapper.readValue(msg, EntityDataUpdate.class); |
|||
Assert.assertEquals(1, update.getCmdId()); |
|||
pageData = update.getData(); |
|||
Assert.assertNotNull(pageData); |
|||
Assert.assertEquals(1, pageData.getData().size()); |
|||
Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId()); |
|||
TsValue[] tsArray = pageData.getData().get(0).getTimeseries().get("temperature"); |
|||
Assert.assertEquals(3, tsArray.length); |
|||
Assert.assertEquals(new TsValue(dataPoint1.getTs(), dataPoint1.getValueAsString()), tsArray[0]); |
|||
Assert.assertEquals(new TsValue(dataPoint2.getTs(), dataPoint2.getValueAsString()), tsArray[1]); |
|||
Assert.assertEquals(new TsValue(dataPoint3.getTs(), dataPoint3.getValueAsString()), tsArray[2]); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,79 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.controller; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.java_websocket.client.WebSocketClient; |
|||
import org.java_websocket.handshake.ServerHandshake; |
|||
|
|||
import java.net.URI; |
|||
import java.nio.channels.NotYetConnectedException; |
|||
import java.util.concurrent.CountDownLatch; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
@Slf4j |
|||
public class TbTestWebSocketClient extends WebSocketClient { |
|||
|
|||
private volatile String lastMsg; |
|||
private volatile boolean replyReceived; |
|||
private CountDownLatch reply; |
|||
|
|||
public TbTestWebSocketClient(URI serverUri) { |
|||
super(serverUri); |
|||
} |
|||
|
|||
@Override |
|||
public void onOpen(ServerHandshake serverHandshake) { |
|||
|
|||
} |
|||
|
|||
@Override |
|||
public void onMessage(String s) { |
|||
if (!replyReceived) { |
|||
replyReceived = true; |
|||
lastMsg = s; |
|||
if (reply != null) { |
|||
reply.countDown(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
@Override |
|||
public void onClose(int i, String s, boolean b) { |
|||
|
|||
} |
|||
|
|||
@Override |
|||
public void onError(Exception e) { |
|||
|
|||
} |
|||
|
|||
@Override |
|||
public void send(String text) throws NotYetConnectedException { |
|||
reply = new CountDownLatch(1); |
|||
replyReceived = false; |
|||
super.send(text); |
|||
} |
|||
|
|||
public String waitForReply() { |
|||
try { |
|||
reply.await(3, TimeUnit.SECONDS); |
|||
} catch (InterruptedException e) { |
|||
log.warn("Failed to await reply", e); |
|||
} |
|||
return lastMsg; |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
/** |
|||
* Copyright © 2016-2020 The Thingsboard Authors |
|||
* |
|||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|||
* you may not use this file except in compliance with the License. |
|||
* You may obtain a copy of the License at |
|||
* |
|||
* http://www.apache.org/licenses/LICENSE-2.0
|
|||
* |
|||
* Unless required by applicable law or agreed to in writing, software |
|||
* distributed under the License is distributed on an "AS IS" BASIS, |
|||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
* See the License for the specific language governing permissions and |
|||
* limitations under the License. |
|||
*/ |
|||
package org.thingsboard.server.controller.sql; |
|||
|
|||
import org.thingsboard.server.controller.BaseEntityQueryControllerTest; |
|||
import org.thingsboard.server.controller.BaseWebsocketApiTest; |
|||
import org.thingsboard.server.dao.service.DaoSqlTest; |
|||
|
|||
@DaoSqlTest |
|||
public class WebsocketApiSqlTest extends BaseWebsocketApiTest { |
|||
} |
|||
Loading…
Reference in new issue