Ver Fonte

设备模块单元测试

ayan há 2 anos atrás
pai
commit
d96c9a54e2
15 ficheiros alterados com 1012 adições e 65 exclusões
  1. 45 0
      docker/run-all/entrypoint.sh
  2. 27 9
      jetlinks-components/common-component/src/main/java/org/jetlinks/community/micrometer/MeterRegistryManager.java
  3. 52 52
      jetlinks-components/configure-component/src/main/java/org/jetlinks/community/configure/device/DeviceClusterConfiguration.java
  4. 3 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/configuration/ElasticSearchThingDataConfiguration.java
  5. 2 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/index/strategies/TimeByDayElasticSearchIndexStrategy.java
  6. 5 4
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/external/socket/WebSocketMessagingHandlerConfiguration.java
  7. 2 0
      jetlinks-components/network-component/network-core/src/main/java/org/jetlinks/community/network/DefaultNetworkManager.java
  8. 2 0
      jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/configuration/RuleEngineLogIndexInitialize.java
  9. 3 0
      jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/event/handler/RuleLogHandler.java
  10. 76 0
      jetlinks-components/timeseries-component/src/main/java/org/jetlinks/community/timeseries/NoneTimeSeriesManager.java
  11. 18 0
      jetlinks-components/timeseries-component/src/main/java/org/jetlinks/community/timeseries/TimeSeriesManagerConfiguration.java
  12. 7 0
      jetlinks-manager/device-manager/pom.xml
  13. 73 0
      jetlinks-manager/device-manager/src/test/java/org/jetlinks/community/device/DeviceTestConfiguration.java
  14. 695 0
      jetlinks-manager/device-manager/src/test/java/org/jetlinks/community/device/web/DeviceInstanceControllerTest.java
  15. 2 0
      jetlinks-standalone/Dockerfile

+ 45 - 0
docker/run-all/entrypoint.sh

@@ -0,0 +1,45 @@
+#!/bin/bash
+#set -x
+#******************************************************************************
+# @file    : entrypoint.sh
+# @author  : wangyubin
+# @date    : 2018-08- 1 10:18:43
+#
+# @brief   : entry point for manage service start order
+# history  : init
+#******************************************************************************
+
+: ${SLEEP_SECOND:=2}
+
+wait_for() {
+    echo Waiting for $1 to listen on $2...
+    while ! nc -z $1 $2; do echo waiting...; sleep $SLEEP_SECOND; done
+}
+
+declare DEPENDS
+declare CMD
+
+while getopts "d:c:" arg
+do
+    case $arg in
+        d)
+            DEPENDS=$OPTARG
+            ;;
+        c)
+            CMD=$OPTARG
+            ;;
+        ?)
+            echo "unkonw argument"
+            exit 1
+            ;;
+    esac
+done
+
+for var in ${DEPENDS//,/ }
+do
+    host=${var%:*}
+    port=${var#*:}
+    wait_for $host $port
+done
+
+eval $CMD

+ 27 - 9
jetlinks-components/common-component/src/main/java/org/jetlinks/community/micrometer/MeterRegistryManager.java

@@ -4,12 +4,14 @@ import io.micrometer.core.instrument.Clock;
 import io.micrometer.core.instrument.MeterRegistry;
 import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
 import lombok.Setter;
+import org.jetlinks.core.metadata.DataType;
+import org.jetlinks.core.metadata.types.StringType;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 /**
@@ -23,17 +25,33 @@ public class MeterRegistryManager {
 
     private Map<String, MeterRegistry> meterRegistryMap = new ConcurrentHashMap<>();
 
-    private List<MeterRegistrySupplier> suppliers;
+    private final List<MeterRegistrySupplier> suppliers;
 
-    private MeterRegistry createMeterRegistry(String metric, String... tagKeys) {
-        return new CompositeMeterRegistry(Clock.SYSTEM,
-            suppliers.stream()
-                .map(supplier -> supplier.getMeterRegistry(metric, tagKeys))
-                .collect(Collectors.toList()));
+
+    public MeterRegistryManager(@Autowired(required = false) List<MeterRegistrySupplier> suppliers) {
+        this.suppliers = suppliers == null ? new ArrayList<>() : suppliers;
+    }
+
+
+    private MeterRegistry createMeterRegistry(String metric, Map<String, DataType> tagDefine) {
+        Map<String, DataType> tags = new HashMap<>(tagDefine);
+        MeterRegistrySettings settings = tags::put;
+        return new CompositeMeterRegistry(Clock.SYSTEM, suppliers
+            .stream()
+            .map(supplier -> supplier.getMeterRegistry(metric))
+            .collect(Collectors.toList()));
     }
 
     public MeterRegistry getMeterRegister(String metric, String... tagKeys) {
-        return meterRegistryMap.computeIfAbsent(metric, _metric -> createMeterRegistry(_metric, tagKeys));
+
+        return meterRegistryMap.computeIfAbsent(metric, _metric -> {
+            if (tagKeys.length == 0) {
+                return createMeterRegistry(metric, Collections.emptyMap());
+            }
+            return createMeterRegistry(metric, Arrays
+                .stream(tagKeys)
+                .collect(Collectors.toMap(Function.identity(), key -> StringType.GLOBAL)));
+        });
     }
 
 }

+ 52 - 52
jetlinks-components/configure-component/src/main/java/org/jetlinks/community/configure/device/DeviceClusterConfiguration.java

@@ -29,58 +29,58 @@ import org.springframework.context.annotation.Configuration;
 @ConditionalOnBean(ProtocolSupports.class)
 public class DeviceClusterConfiguration {
 
-    @Bean
-    public ClusterDeviceRegistry deviceRegistry(ProtocolSupports supports,
-                                                ClusterManager manager,
-                                                ConfigStorageManager storageManager,
-                                                DeviceOperationBroker handler) {
-
-        return new ClusterDeviceRegistry(supports,
-                                         storageManager,
-                                         manager,
-                                         handler,
-                                         CaffeinatedGuava.build(Caffeine.newBuilder()));
-    }
-
-
-    @Bean
-    @ConditionalOnBean(ClusterDeviceRegistry.class)
-    public BeanPostProcessor interceptorRegister(ClusterDeviceRegistry registry) {
-        return new BeanPostProcessor() {
-            @Override
-            public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
-                if (bean instanceof DeviceMessageSenderInterceptor) {
-                    registry.addInterceptor(((DeviceMessageSenderInterceptor) bean));
-                }
-                if (bean instanceof DeviceStateChecker) {
-                    registry.addStateChecker(((DeviceStateChecker) bean));
-                }
-                return bean;
-            }
-        };
-    }
-
-    @Bean(initMethod = "init", destroyMethod = "shutdown")
-    @ConditionalOnBean(RpcManager.class)
-    public PersistenceDeviceSessionManager deviceSessionManager(RpcManager rpcManager) {
-
-        return new PersistenceDeviceSessionManager(rpcManager);
-    }
-
-    @ConditionalOnBean(DecodedClientMessageHandler.class)
-    @Bean
-    public ClusterSendToDeviceMessageHandler defaultSendToDeviceMessageHandler(DeviceSessionManager sessionManager,
-                                                                               DeviceRegistry registry,
-                                                                               MessageHandler messageHandler,
-                                                                               DecodedClientMessageHandler clientMessageHandler) {
-        return new ClusterSendToDeviceMessageHandler(sessionManager, messageHandler, registry, clientMessageHandler);
-    }
-
-    @Bean
-    public RpcDeviceOperationBroker rpcDeviceOperationBroker(RpcManager rpcManager,
-                                                             DeviceSessionManager sessionManager) {
-        return new RpcDeviceOperationBroker(rpcManager, sessionManager);
-    }
+//    @Bean
+//    public ClusterDeviceRegistry deviceRegistry(ProtocolSupports supports,
+//                                                ClusterManager manager,
+//                                                ConfigStorageManager storageManager,
+//                                                DeviceOperationBroker handler) {
+//
+//        return new ClusterDeviceRegistry(supports,
+//                                         storageManager,
+//                                         manager,
+//                                         handler,
+//                                         CaffeinatedGuava.build(Caffeine.newBuilder()));
+//    }
+//
+//
+//    @Bean
+//    @ConditionalOnBean(ClusterDeviceRegistry.class)
+//    public BeanPostProcessor interceptorRegister(ClusterDeviceRegistry registry) {
+//        return new BeanPostProcessor() {
+//            @Override
+//            public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+//                if (bean instanceof DeviceMessageSenderInterceptor) {
+//                    registry.addInterceptor(((DeviceMessageSenderInterceptor) bean));
+//                }
+//                if (bean instanceof DeviceStateChecker) {
+//                    registry.addStateChecker(((DeviceStateChecker) bean));
+//                }
+//                return bean;
+//            }
+//        };
+//    }
+//
+//    @Bean(initMethod = "init", destroyMethod = "shutdown")
+//    @ConditionalOnBean(RpcManager.class)
+//    public PersistenceDeviceSessionManager deviceSessionManager(RpcManager rpcManager) {
+//
+//        return new PersistenceDeviceSessionManager(rpcManager);
+//    }
+//
+//    @ConditionalOnBean(DecodedClientMessageHandler.class)
+//    @Bean
+//    public ClusterSendToDeviceMessageHandler defaultSendToDeviceMessageHandler(DeviceSessionManager sessionManager,
+//                                                                               DeviceRegistry registry,
+//                                                                               MessageHandler messageHandler,
+//                                                                               DecodedClientMessageHandler clientMessageHandler) {
+//        return new ClusterSendToDeviceMessageHandler(sessionManager, messageHandler, registry, clientMessageHandler);
+//    }
+//
+//    @Bean
+//    public RpcDeviceOperationBroker rpcDeviceOperationBroker(RpcManager rpcManager,
+//                                                             DeviceSessionManager sessionManager) {
+//        return new RpcDeviceOperationBroker(rpcManager, sessionManager);
+//    }
 
 
 }

+ 3 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/configuration/ElasticSearchThingDataConfiguration.java

@@ -7,6 +7,7 @@ import org.jetlinks.community.elastic.search.service.ElasticSearchService;
 import org.jetlinks.community.elastic.search.things.ElasticSearchColumnModeStrategy;
 import org.jetlinks.community.elastic.search.things.ElasticSearchRowModeStrategy;
 import org.jetlinks.community.things.data.ThingsDataRepositoryStrategy;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -16,6 +17,7 @@ import org.springframework.context.annotation.Configuration;
 public class ElasticSearchThingDataConfiguration {
 
     @Bean
+    @ConditionalOnBean(ElasticSearchService.class)
     public ElasticSearchColumnModeStrategy elasticSearchColumnModThingDataPolicy(
         ThingsRegistry registry,
         ElasticSearchService searchService,
@@ -26,6 +28,7 @@ public class ElasticSearchThingDataConfiguration {
     }
 
     @Bean
+    @ConditionalOnBean(ElasticSearchService.class)
     public ElasticSearchRowModeStrategy elasticSearchRowModThingDataPolicy(
         ThingsRegistry registry,
         ElasticSearchService searchService,

+ 2 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/index/strategies/TimeByDayElasticSearchIndexStrategy.java

@@ -3,6 +3,7 @@ package org.jetlinks.community.elastic.search.index.strategies;
 import org.hswebframework.utils.time.DateFormatter;
 import org.jetlinks.community.elastic.search.index.ElasticSearchIndexProperties;
 import org.jetlinks.community.elastic.search.service.reactive.ReactiveElasticsearchClient;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
 import org.springframework.stereotype.Component;
 
 import java.time.LocalDate;
@@ -15,6 +16,7 @@ import java.util.Date;
  * @since 1.0
  */
 @Component
+@ConditionalOnBean(ReactiveElasticsearchClient.class)
 public class TimeByDayElasticSearchIndexStrategy extends TemplateElasticSearchIndexStrategy {
 
     public TimeByDayElasticSearchIndexStrategy(ReactiveElasticsearchClient client, ElasticSearchIndexProperties properties) {

+ 5 - 4
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/external/socket/WebSocketMessagingHandlerConfiguration.java

@@ -3,6 +3,7 @@ package org.jetlinks.community.gateway.external.socket;
 import org.hswebframework.web.authorization.ReactiveAuthenticationManager;
 import org.hswebframework.web.authorization.token.UserTokenManager;
 import org.jetlinks.community.gateway.external.MessagingManager;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -16,10 +17,10 @@ import java.util.HashMap;
 import java.util.Map;
 
 @Configuration
-//@ConditionalOnBean({
-//    ReactiveAuthenticationManager.class,
-//    UserTokenManager.class
-//})
+@ConditionalOnBean({
+    ReactiveAuthenticationManager.class,
+    UserTokenManager.class
+})
 public class WebSocketMessagingHandlerConfiguration {
 
 

+ 2 - 0
jetlinks-components/network-component/network-core/src/main/java/org/jetlinks/community/network/DefaultNetworkManager.java

@@ -8,6 +8,7 @@ import org.jetlinks.core.event.Subscription;
 import org.springframework.beans.BeansException;
 import org.springframework.beans.factory.config.BeanPostProcessor;
 import org.springframework.boot.CommandLineRunner;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
 import org.springframework.stereotype.Component;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
@@ -30,6 +31,7 @@ import java.util.concurrent.ConcurrentHashMap;
  */
 @Component
 @Slf4j
+@ConditionalOnBean(NetworkConfigManager.class)
 public class DefaultNetworkManager implements NetworkManager, BeanPostProcessor, CommandLineRunner {
 
     private final NetworkConfigManager configManager;

+ 2 - 0
jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/configuration/RuleEngineLogIndexInitialize.java

@@ -6,6 +6,7 @@ import org.jetlinks.community.elastic.search.index.ElasticSearchIndexManager;
 import org.jetlinks.community.rule.engine.event.handler.RuleEngineLoggerIndexProvider;
 import org.jetlinks.core.metadata.types.DateTimeType;
 import org.jetlinks.core.metadata.types.StringType;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
 import org.springframework.core.annotation.Order;
 import org.springframework.stereotype.Component;
 
@@ -16,6 +17,7 @@ import org.springframework.stereotype.Component;
 @Component
 @Order(1)
 @Slf4j
+@ConditionalOnBean(DefaultElasticSearchIndexMetadata.class)
 public class RuleEngineLogIndexInitialize {
 
     public RuleEngineLogIndexInitialize(ElasticSearchIndexManager indexManager) {

+ 3 - 0
jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/event/handler/RuleLogHandler.java

@@ -2,11 +2,13 @@ package org.jetlinks.community.rule.engine.event.handler;
 
 import lombok.extern.slf4j.Slf4j;
 import org.jetlinks.community.elastic.search.service.ElasticSearchService;
+import org.jetlinks.community.elastic.search.service.reactive.ReactiveElasticsearchClient;
 import org.jetlinks.community.gateway.annotation.Subscribe;
 import org.jetlinks.community.rule.engine.entity.RuleEngineExecuteEventInfo;
 import org.jetlinks.core.event.TopicPayload;
 import org.jetlinks.rule.engine.defaults.LogEvent;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
 import org.springframework.core.annotation.Order;
 import org.springframework.stereotype.Component;
 import reactor.core.publisher.Mono;
@@ -14,6 +16,7 @@ import reactor.core.publisher.Mono;
 @Component
 @Slf4j
 @Order(3)
+@ConditionalOnBean(ElasticSearchService.class)
 public class RuleLogHandler {
 
     @Autowired

+ 76 - 0
jetlinks-components/timeseries-component/src/main/java/org/jetlinks/community/timeseries/NoneTimeSeriesManager.java

@@ -0,0 +1,76 @@
+package org.jetlinks.community.timeseries;
+
+import org.hswebframework.ezorm.core.param.QueryParam;
+import org.jetlinks.community.timeseries.query.AggregationData;
+import org.jetlinks.community.timeseries.query.AggregationQueryParam;
+import org.reactivestreams.Publisher;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.Collection;
+
+public class NoneTimeSeriesManager implements TimeSeriesManager {
+    public static final NoneTimeSeriesService NONE = new NoneTimeSeriesService();
+
+    @Override
+    public TimeSeriesService getService(TimeSeriesMetric metric) {
+        return NONE;
+    }
+
+    @Override
+    public TimeSeriesService getServices(TimeSeriesMetric... metric) {
+        return NONE;
+    }
+
+    @Override
+    public TimeSeriesService getServices(String... metric) {
+        return NONE;
+    }
+
+    @Override
+    public TimeSeriesService getService(String metric) {
+        return NONE;
+    }
+
+    @Override
+    public Mono<Void> registerMetadata(TimeSeriesMetadata metadata) {
+        return Mono.empty();
+    }
+
+    static class NoneTimeSeriesService implements TimeSeriesService {
+        @Override
+        public Flux<TimeSeriesData> query(QueryParam queryParam) {
+            return Flux.empty();
+        }
+
+        @Override
+        public Flux<TimeSeriesData> multiQuery(Collection<QueryParam> query) {
+            return Flux.empty();
+        }
+
+        @Override
+        public Mono<Integer> count(QueryParam queryParam) {
+            return Mono.empty();
+        }
+
+        @Override
+        public Flux<AggregationData> aggregation(AggregationQueryParam queryParam) {
+            return Flux.empty();
+        }
+
+        @Override
+        public Mono<Void> commit(Publisher<TimeSeriesData> data) {
+            return Mono.empty();
+        }
+
+        @Override
+        public Mono<Void> commit(TimeSeriesData data) {
+            return Mono.empty();
+        }
+
+        @Override
+        public Mono<Void> save(Publisher<TimeSeriesData> data) {
+            return Mono.empty();
+        }
+    }
+}

+ 18 - 0
jetlinks-components/timeseries-component/src/main/java/org/jetlinks/community/timeseries/TimeSeriesManagerConfiguration.java

@@ -0,0 +1,18 @@
+package org.jetlinks.community.timeseries;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import reactor.core.publisher.Flux;
+
+import java.time.Duration;
+
+@Configuration(proxyBeanMethods = false)
+public class TimeSeriesManagerConfiguration {
+
+    @ConditionalOnMissingBean(TimeSeriesManager.class)
+    @Bean
+    public NoneTimeSeriesManager timeSeriesManager() {
+        return new NoneTimeSeriesManager();
+    }
+}

+ 7 - 0
jetlinks-manager/device-manager/pom.xml

@@ -99,6 +99,13 @@
             <scope>compile</scope>
         </dependency>
 
+        <dependency>
+            <groupId>org.jetlinks.community</groupId>
+            <artifactId>test-component</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+
     </dependencies>
 
 </project>

+ 73 - 0
jetlinks-manager/device-manager/src/test/java/org/jetlinks/community/device/DeviceTestConfiguration.java

@@ -0,0 +1,73 @@
+package org.jetlinks.community.device;
+
+import org.hswebframework.web.authorization.token.DefaultUserTokenManager;
+import org.hswebframework.web.authorization.token.UserTokenManager;
+import org.hswebframework.web.starter.jackson.CustomCodecsAutoConfiguration;
+import org.jetlinks.community.configure.cluster.ClusterConfiguration;
+import org.jetlinks.community.configure.device.DeviceClusterConfiguration;
+import org.jetlinks.community.elastic.search.configuration.ElasticSearchConfiguration;
+import org.jetlinks.core.ProtocolSupports;
+import org.jetlinks.core.device.DeviceRegistry;
+import org.jetlinks.core.device.StandaloneDeviceMessageBroker;
+import org.jetlinks.core.device.session.DeviceSessionManager;
+import org.jetlinks.core.server.MessageHandler;
+import org.jetlinks.supports.device.session.LocalDeviceSessionManager;
+import org.jetlinks.supports.official.JetLinksDeviceMetadataCodec;
+import org.jetlinks.supports.test.InMemoryDeviceRegistry;
+import org.jetlinks.supports.test.MockProtocolSupport;
+import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
+import org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration;
+import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@ImportAutoConfiguration({
+    CodecsAutoConfiguration.class,
+    JacksonAutoConfiguration.class,
+    CustomCodecsAutoConfiguration.class,
+    ClusterConfiguration.class,
+    DeviceClusterConfiguration.class
+})
+public class DeviceTestConfiguration {
+
+    @Bean
+    public ProtocolSupports mockProtocolSupport(){
+        return new MockProtocolSupport();
+    }
+
+    @Bean
+    public UserTokenManager userTokenManager(){
+        return new DefaultUserTokenManager();
+    }
+
+    @Bean
+    public DeviceRegistry deviceRegistry() {
+
+        return new InMemoryDeviceRegistry();
+    }
+
+    @Bean
+    public MessageHandler messageHandler() {
+
+        return new StandaloneDeviceMessageBroker();
+    }
+
+    @Bean
+    public DeviceSessionManager deviceSessionManager() {
+
+        return LocalDeviceSessionManager.create();
+    }
+
+    @Bean
+    public ElasticSearchConfiguration searchConfiguration() {
+
+        return new ElasticSearchConfiguration();
+    }
+
+    @Bean
+    public JetLinksDeviceMetadataCodec jetLinksDeviceMetadataCodec(){
+        return new JetLinksDeviceMetadataCodec();
+    }
+
+}

+ 695 - 0
jetlinks-manager/device-manager/src/test/java/org/jetlinks/community/device/web/DeviceInstanceControllerTest.java

@@ -0,0 +1,695 @@
+package org.jetlinks.community.device.web;
+
+import com.alibaba.fastjson.JSON;
+import lombok.SneakyThrows;
+import org.jetlinks.community.test.spring.TestJetLinksController;
+import org.jetlinks.core.metadata.SimplePropertyMetadata;
+import org.jetlinks.core.metadata.types.FloatType;
+import org.jetlinks.community.PropertyMetadataConstants;
+import org.jetlinks.community.PropertyMetric;
+import org.jetlinks.community.device.entity.DeviceInstanceEntity;
+import org.jetlinks.community.device.entity.DeviceProductEntity;
+import org.jetlinks.community.device.entity.DeviceTagEntity;
+import org.jetlinks.community.device.service.LocalDeviceInstanceService;
+import org.jetlinks.community.device.service.LocalDeviceProductService;
+import org.jetlinks.community.device.service.data.DeviceDataService;
+import org.jetlinks.community.device.web.request.AggRequest;
+import org.jetlinks.community.relation.entity.RelationEntity;
+import org.jetlinks.community.relation.service.RelatedObjectInfo;
+import org.jetlinks.community.relation.service.RelationService;
+import org.jetlinks.community.relation.service.request.SaveRelationRequest;
+import org.jetlinks.community.timeseries.query.Aggregation;
+import org.jetlinks.supports.official.JetLinksDeviceMetadata;
+import org.jetlinks.supports.official.JetLinksDeviceMetadataCodec;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.http.MediaType;
+import org.springframework.util.StringUtils;
+import reactor.test.StepVerifier;
+
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+@WebFluxTest(value = DeviceInstanceController.class, properties = {
+    "spring.reactor.debug-agent.enabled=true"
+})
+class DeviceInstanceControllerTest extends TestJetLinksController {
+
+    @Autowired
+    @SuppressWarnings("all")
+    private LocalDeviceInstanceService deviceService;
+
+    @Autowired
+    @SuppressWarnings("all")
+    private LocalDeviceProductService productService;
+
+    private String deviceId;
+    private String productId;
+
+    private String metadata;
+    @BeforeEach
+    void setup() {
+        DeviceProductEntity product = new DeviceProductEntity();
+        product.setMetadata("{}");
+        product.setTransportProtocol("MQTT");
+        product.setMessageProtocol("test");
+        product.setId(productId = "deviceinstancecontrollertest_product");
+        product.setName("DeviceInstanceControllerTest");
+
+        JetLinksDeviceMetadata metadata = new JetLinksDeviceMetadata("Test", "Test");
+        {
+            SimplePropertyMetadata metric = SimplePropertyMetadata.of(
+                "metric", "Metric", FloatType.GLOBAL
+            );
+            metric.setExpands(
+                PropertyMetadataConstants.Metrics
+                    .metricsToExpands(Arrays.asList(
+                        PropertyMetric.of("max", "最大值", 100),
+                        PropertyMetric.of("min", "最小值", -100)
+                    ))
+            );
+            metadata.addProperty(metric);
+        }
+
+        product.setMetadata(this.metadata=JetLinksDeviceMetadataCodec.getInstance().doEncode(metadata));
+
+        productService
+            .save(product)
+            .then(productService.deploy(productId))
+            .then()
+            .as(StepVerifier::create)
+            .expectComplete()
+            .verify();
+
+        DeviceInstanceEntity device = new DeviceInstanceEntity();
+        device.setId(deviceId = "deviceinstancecontrollertest_device");
+        device.setName("DeviceInstanceControllerTest");
+        device.setProductId(product.getId());
+        device.setProductName(device.getName());
+
+        client
+            .patch()
+            .uri("/device/instance")
+            .bodyValue(device)
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+
+        client
+            .put()
+            .uri("/device/instance/batch/_deploy")
+            .contentType(MediaType.APPLICATION_JSON)
+            .bodyValue(Arrays.asList(deviceId))
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+
+
+    }
+
+    @AfterEach
+    void shutdown() {
+        client
+            .put()
+            .uri("/device/instance/batch/_unDeploy")
+            .contentType(MediaType.APPLICATION_JSON)
+            .bodyValue(Arrays.asList(deviceId))
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+
+        client
+            .put()
+            .uri("/device/instance/batch/_delete")
+            .contentType(MediaType.APPLICATION_JSON)
+            .bodyValue(Arrays.asList(deviceId))
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+    }
+
+    @Test
+    @SneakyThrows
+    void testCommon() {
+//        {
+//            DeviceInstanceEntity device = new DeviceInstanceEntity();
+//            device.setId(deviceId);
+//            device.setName("DeviceInstanceControllerTest");
+//            device.setProductId(productId);
+//            device.setProductName(device.getName());
+//            //重复创建
+//            client
+//                .post()
+//                .uri("/device/instance")
+//                .bodyValue(device)
+//                .exchange()
+//                .expectStatus()
+//                .is4xxClientError();
+//        }
+        client
+            .get()
+            .uri("/device/instance/{id:.+}/detail", deviceId)
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+
+        client
+            .get()
+            .uri("/device/instance/bind-providers")
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+
+
+        client
+            .get()
+            .uri("/device/instance/{deviceId}/state", deviceId)
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+        client
+            .get()
+            .uri("/device/instance/state/_sync")
+            .accept(MediaType.TEXT_EVENT_STREAM)
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+
+
+        client
+            .post()
+            .uri("/device/instance/{deviceId}/deploy", deviceId)
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+
+
+        client
+            .post()
+            .uri("/device/instance/{deviceId}/undeploy", deviceId)
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+
+        client
+            .post()
+            .uri("/device/instance/{deviceId}/disconnect", deviceId)
+            .exchange();
+    }
+
+    @Test
+    void testProperties() {
+
+        client
+            .get()
+            .uri("/device/instance/{deviceId:.+}/properties/_query?where=property is test", deviceId)
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+
+        client
+            .get()
+            .uri("/device/instance/{deviceId:.+}/properties/_query", deviceId)
+            .exchange()
+            .expectStatus()
+            .is4xxClientError();
+
+        client
+            .get()
+            .uri("/device/instance/{deviceId:.+}/properties/latest", deviceId)
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+
+        client
+            .get()
+            .uri("/device/instance/{deviceId:.+}/properties", deviceId)
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+
+        client
+            .post()
+            .uri("/device/instance/{deviceId:.+}/properties/_top/{numberOfTop}", deviceId, 1)
+            .contentType(MediaType.APPLICATION_JSON)
+            .bodyValue("{}")
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+
+        client
+            .get()
+            .uri("/device/instance/{deviceId:.+}/property/{property}/_query", deviceId, "test")
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+
+        client
+            .post()
+            .uri("/device/instance/{deviceId:.+}/property/{property}/_query", deviceId, "test")
+            .contentType(MediaType.APPLICATION_JSON)
+            .bodyValue("{}")
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+
+        client
+            .post()
+            .uri("/device/instance/{deviceId:.+}/properties/_query", deviceId)
+            .contentType(MediaType.APPLICATION_JSON)
+            .bodyValue("{}")
+            .exchange();
+
+
+        client
+            .get()
+            .uri("/device/instance/{deviceId:.+}/property/{property:.+}", deviceId, "test")
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+
+        AggRequest request = new AggRequest();
+
+        request.setColumns(Arrays.asList(
+            new DeviceDataService.DevicePropertyAggregation("test", "alias", Aggregation.AVG)
+        ));
+        request.setQuery(DeviceDataService.AggregationRequest
+                             .builder()
+                             .build());
+
+        client
+            .post()
+            .uri("/device/instance/{deviceId:.+}/agg/_query", deviceId)
+            .contentType(MediaType.APPLICATION_JSON)
+            .bodyValue(request)
+            .exchange();
+
+
+    }
+
+    @Test
+    void testEvent() {
+        client
+            .get()
+            .uri("/device/instance/{deviceId:.+}/event/{eventId}", deviceId, "test")
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+        client
+            .post()
+            .uri("/device/instance/{deviceId:.+}/event/{eventId}", deviceId, "test")
+            .contentType(MediaType.APPLICATION_JSON)
+            .bodyValue("{}")
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+    }
+
+    @Test
+    void testLog() {
+        client
+            .get()
+            .uri("/device/instance/{deviceId:.+}/logs", deviceId, "test")
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+        client
+            .post()
+            .uri("/device/instance/{deviceId:.+}/logs", deviceId, "test")
+            .contentType(MediaType.APPLICATION_JSON)
+            .bodyValue("{}")
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+    }
+
+    @Test
+    void testTag() {
+        DeviceTagEntity tag = new DeviceTagEntity();
+        tag.setKey("test");
+        tag.setValue("value");
+
+        client
+            .patch()
+            .uri("/device/instance/{deviceId}/tag", deviceId)
+            .contentType(MediaType.APPLICATION_JSON)
+            .bodyValue(tag)
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+
+        List<DeviceTagEntity> tags = client
+            .get()
+            .uri("/device/instance/{deviceId}/tags", deviceId)
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful()
+            .expectBodyList(DeviceTagEntity.class)
+            .returnResult()
+            .getResponseBody();
+
+        assertNotNull(tags);
+        assertFalse(tags.isEmpty());
+
+        client
+            .delete()
+            .uri("/device/instance/{deviceId}/tag/{tagId}", deviceId, tags.get(0).getId())
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+    }
+
+    @Test
+    @SneakyThrows
+    void testMetadata() {
+        client
+            .get()
+            .uri("/device/instance/{id:.+}/config-metadata", deviceId)
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+
+        client
+            .get()
+            .uri("/device/instance/{id:.+}/config-metadata/{metadataType}/{metadataId}/{typeId}",
+                 deviceId,
+                 "property",
+                 "temp",
+                 "test")
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+
+
+        String metadata = client
+            .post()
+            .uri("/device/instance/{deviceId}/property-metadata/import?fileUrl=" + new ClassPathResource("property-metadata.csv")
+                .getFile()
+                .getAbsolutePath(), deviceId)
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful()
+            .expectBody(String.class)
+            .returnResult()
+            .getResponseBody();
+        assertNotNull(metadata);
+        client
+            .put()
+            .uri("/device/instance/{id:.+}/metadata", deviceId)
+            .contentType(MediaType.APPLICATION_JSON)
+            .bodyValue(metadata)
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+
+        deviceService.findById(deviceId)
+                     .as(StepVerifier::create)
+                     .expectNextMatches(device -> Objects.equals(
+                         device.getDeriveMetadata(),
+                         metadata
+                     ))
+                     .expectComplete()
+                     .verify();
+        client
+            .put()
+            .uri("/device/instance/{id}/metadata/merge-product", deviceId)
+            .contentType(MediaType.APPLICATION_JSON)
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+
+        client
+            .delete()
+            .uri("/device/instance/{id}/metadata", deviceId)
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+
+        deviceService
+            .findById(deviceId)
+            .as(StepVerifier::create)
+            .expectNextMatches(device -> StringUtils.isEmpty(device.getDeriveMetadata()))
+            .expectComplete()
+            .verify();
+    }
+
+    @Test
+    void testConfiguration() {
+
+        deviceService.deploy(deviceId)
+                     .then()
+                     .as(StepVerifier::create)
+                     .expectComplete()
+                     .verify();
+        client
+            .post()
+            .uri("/device/instance/{deviceId:.+}/configuration/_write", deviceId)
+            .contentType(MediaType.APPLICATION_JSON)
+            .bodyValue("{\"test\":\"123\"}")
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+
+        client
+            .post()
+            .uri("/device/instance/{deviceId:.+}/configuration/_read", deviceId)
+            .contentType(MediaType.APPLICATION_JSON)
+            .bodyValue("[\"test\"]")
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful()
+            .expectBody(Map.class)
+            .isEqualTo(Collections.singletonMap("test", "123"));
+
+        client
+            .put()
+            .uri("/device/instance/{deviceId:.+}/configuration/_reset", deviceId)
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+
+        client
+            .put()
+            .uri("/device/instance/{deviceId:.+}/shadow", deviceId)
+            .contentType(MediaType.APPLICATION_JSON)
+            .bodyValue("{\"test\":\"123\"}")
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+
+        client
+            .get()
+            .uri("/device/instance/{deviceId:.+}/shadow", deviceId)
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful()
+            .expectBody(String.class)
+            .isEqualTo("{\"test\":\"123\"}");
+
+    }
+
+    @Test
+    void testCommand() {
+        client
+            .put()
+            .uri("/device/instance/{deviceId:.+}/property", deviceId)
+            .contentType(MediaType.APPLICATION_JSON)
+            .bodyValue(Collections.singletonMap("test", "value"))
+            .exchange();
+
+        client
+            .post()
+            .uri("/device/instance/{deviceId:.+}/property/_read", deviceId)
+            .contentType(MediaType.APPLICATION_JSON)
+            .bodyValue(Collections.singleton("test"))
+            .exchange();
+
+
+        client
+            .post()
+            .uri("/device/instance/{deviceId:.+}/function/test", deviceId)
+            .contentType(MediaType.APPLICATION_JSON)
+            .bodyValue(Collections.singletonMap("test", "value"))
+            .exchange();
+
+        client
+            .post()
+            .uri("/device/instance/{deviceId:.+}/message", deviceId)
+            .contentType(MediaType.APPLICATION_JSON)
+            .bodyValue(Collections.singletonMap("properties", Collections.singletonList("test")))
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+
+        Map<String, Object> data = new HashMap<>();
+        data.put("deviceId", "test");
+        data.put("properties", Collections.singletonList("test"));
+
+        client
+            .post()
+            .uri("/device/instance/messages", deviceId)
+            .contentType(MediaType.APPLICATION_JSON)
+            .bodyValue(data)
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+
+    }
+
+    @Test
+    void testAutoChangeProductInfo() {
+        client.post()
+              .uri("/device/instance")
+              .contentType(MediaType.APPLICATION_JSON)
+              .bodyValue("{\"id\":\"testAutoChangeProductInfo\",\"name\":\"Test\",\"productId\":\"" + productId + "\"}")
+              .exchange()
+              .expectStatus()
+              .is2xxSuccessful();
+
+    }
+
+    @Autowired
+    private RelationService relationService;
+
+    @Test
+    void testRelation() {
+        RelationEntity entity = new RelationEntity();
+        entity.setRelation("manager");
+        entity.setObjectType("device");
+        entity.setObjectTypeName("设备");
+        entity.setTargetType("user");
+        entity.setTargetTypeName("用户");
+        entity.setName("管理员");
+        relationService
+            .save(entity).block();
+
+
+        client.get()
+              .uri("/device/instance/{deviceId}/detail", deviceId)
+              .exchange()
+              .expectStatus()
+              .is2xxSuccessful()
+              .expectBody()
+              .jsonPath("$.relations[0].relation").isEqualTo("manager")
+              .jsonPath("$.relations[0].related").isEmpty();
+
+
+        client.patch()
+              .uri("/device/instance/{deviceId}/relations", deviceId)
+              .contentType(MediaType.APPLICATION_JSON)
+              .bodyValue(SaveRelationRequest.of(
+                  "user",
+                  "manager",
+                  Arrays.asList(RelatedObjectInfo.of("admin", "管理员")),
+                  null
+              ))
+              .exchange()
+              .expectStatus()
+              .is2xxSuccessful();
+
+        client.get()
+              .uri("/device/instance/{deviceId}/detail", deviceId)
+              .exchange()
+              .expectStatus()
+              .is2xxSuccessful()
+              .expectBody()
+              .jsonPath("$.relations[0].relation").isEqualTo("manager")
+              .jsonPath("$.relations[0].related[0].id").isEqualTo("admin")
+              .jsonPath("$.relations[0].related[0].name").isEqualTo("管理员");
+
+
+    }
+
+    @Test
+    void testMetric() {
+        client.get()
+              .uri("/device/instance/{deviceId}/metric/property/{property}", deviceId, "metric")
+              .exchange()
+              .expectStatus()
+              .is2xxSuccessful()
+              .expectBody()
+              .jsonPath("[0].id").isEqualTo("max")
+              .jsonPath("[0].value").isEqualTo(100)
+              .jsonPath("[1].id").isEqualTo("min")
+              .jsonPath("[1].value").isEqualTo(-100);
+
+
+        client.patch()
+              .uri("/device/instance/{deviceId}/metric/property/{property}", deviceId, "metric")
+              .contentType(MediaType.APPLICATION_JSON)
+              .bodyValue(PropertyMetric.of("max", "最大值", 110))
+              .exchange()
+              .expectStatus()
+              .is2xxSuccessful();
+
+        client.get()
+              .uri("/device/instance/{deviceId}/metric/property/{property}", deviceId, "metric")
+              .exchange()
+              .expectStatus()
+              .is2xxSuccessful()
+              .expectBody()
+              .jsonPath("[0].id").isEqualTo("max")
+              .jsonPath("[0].value").isEqualTo(110)
+              .jsonPath("[1].id").isEqualTo("min")
+              .jsonPath("[1].value").isEqualTo(-100);
+
+
+    }
+
+    @Test
+    void testDetail(){
+        client
+            .get()
+            .uri("/device/instance/{deviceId}/detail", deviceId)
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful()
+            .expectBody()
+            .jsonPath("$.metadata").isEqualTo(metadata);
+
+        String newMetadata="{\"properties\":[]}";
+
+        client
+            .put()
+            .uri("/device/product/{productId}", productId)
+            .contentType(MediaType.APPLICATION_JSON)
+            .bodyValue("{\"metadata\":"+ JSON.toJSONString(newMetadata)+"}")
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+
+        //仅保存 未发布
+        client
+            .get()
+            .uri("/device/instance/{deviceId}/detail", deviceId)
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful()
+            .expectBody()
+            .jsonPath("$.metadata").isEqualTo(metadata);
+
+        client
+            .post()
+            .uri("/device/product/{productId}/deploy", productId)
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful();
+
+        client
+            .get()
+            .uri("/device/instance/{deviceId}/detail", deviceId)
+            .exchange()
+            .expectStatus()
+            .is2xxSuccessful()
+            .expectBody()
+            .jsonPath("$.metadata").isEqualTo(newMetadata);
+
+
+    }
+}

+ 2 - 0
jetlinks-standalone/Dockerfile

@@ -11,5 +11,7 @@ COPY --from=builder application/snapshot-dependencies/ ./
 COPY --from=builder application/spring-boot-loader/ ./
 COPY --from=builder application/application/ ./
 COPY docker-entrypoint.sh ./
+COPY entrypoint.sh ./
 RUN chmod +x docker-entrypoint.sh
+RUN chmod +x entrypoint.sh
 ENTRYPOINT ["./docker-entrypoint.sh"]