diff --git a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java index 7496eaf945..db0ed47c56 100644 --- a/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java +++ b/application/src/test/java/org/thingsboard/server/service/sync/vc/VersionControlTest.java @@ -372,6 +372,8 @@ public class VersionControlTest extends AbstractControllerTest { "\"" + aliasId + "\": {\n" + "\"alias\": \"assets\",\n" + "\"filter\": {\n" + + " \"type\": \"entityList\",\n" + + " \"entityType\": \"ASSET\",\n" + " \"entityList\": [\n" + " \"" + asset1.getId() + "\",\n" + " \"" + asset2.getId() + "\",\n" + diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/DashboardConfig.java b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/DashboardConfig.java new file mode 100644 index 0000000000..5a6b093510 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/DashboardConfig.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2026 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.common.data.dashboard; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.validation.Valid; +import lombok.Data; + +import java.util.Map; +import java.util.UUID; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class DashboardConfig { + + private Map entityAliases; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/EntityAlias.java b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/EntityAlias.java new file mode 100644 index 0000000000..1adbeb3c80 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/EntityAlias.java @@ -0,0 +1,41 @@ +/** + * Copyright © 2016-2026 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.common.data.dashboard; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.thingsboard.server.common.data.dashboard.filter.DashboardAliasFilter; + +import java.util.UUID; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class EntityAlias { + + @NotNull + private UUID id; + + @NotBlank + private String alias; + + @NotNull + @Valid + private DashboardAliasFilter filter; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardAliasFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardAliasFilter.java new file mode 100644 index 0000000000..16b81b9acb --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardAliasFilter.java @@ -0,0 +1,50 @@ +/** + * Copyright © 2016-2026 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.common.data.dashboard.filter; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type") +@JsonSubTypes({ + @JsonSubTypes.Type(value = DashboardSingleEntityFilter.class, name = "singleEntity"), + @JsonSubTypes.Type(value = DashboardEntityListFilter.class, name = "entityList"), + @JsonSubTypes.Type(value = DashboardEntityNameFilter.class, name = "entityName"), + @JsonSubTypes.Type(value = DashboardEntityTypeFilter.class, name = "entityType"), + @JsonSubTypes.Type(value = DashboardStateEntityFilter.class, name = "stateEntity"), + @JsonSubTypes.Type(value = DashboardAssetTypeFilter.class, name = "assetType"), + @JsonSubTypes.Type(value = DashboardDeviceTypeFilter.class, name = "deviceType"), + @JsonSubTypes.Type(value = DashboardEdgeTypeFilter.class, name = "edgeType"), + @JsonSubTypes.Type(value = DashboardEntityViewTypeFilter.class, name = "entityViewType"), + @JsonSubTypes.Type(value = DashboardApiUsageStateFilter.class, name = "apiUsageState"), + @JsonSubTypes.Type(value = DashboardRelationsQueryFilter.class, name = "relationsQuery"), + @JsonSubTypes.Type(value = DashboardAssetSearchQueryFilter.class, name = "assetSearchQuery"), + @JsonSubTypes.Type(value = DashboardDeviceSearchQueryFilter.class, name = "deviceSearchQuery"), + @JsonSubTypes.Type(value = DashboardEntityViewSearchQueryFilter.class, name = "entityViewSearchQuery"), + @JsonSubTypes.Type(value = DashboardEdgeSearchQueryFilter.class, name = "edgeSearchQuery") +}) +public abstract class DashboardAliasFilter { + + private boolean resolveMultiple; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardApiUsageStateFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardApiUsageStateFilter.java new file mode 100644 index 0000000000..a34f933b26 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardApiUsageStateFilter.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2026 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.common.data.dashboard.filter; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.thingsboard.server.common.data.id.CustomerId; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DashboardApiUsageStateFilter extends DashboardAliasFilter { + + private CustomerId customerId; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardAssetSearchQueryFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardAssetSearchQueryFilter.java new file mode 100644 index 0000000000..7454b7b8bd --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardAssetSearchQueryFilter.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2026 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.common.data.dashboard.filter; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DashboardAssetSearchQueryFilter extends DashboardEntitySearchQueryFilter { + + @NotEmpty + private List<@NotBlank String> assetTypes; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardAssetTypeFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardAssetTypeFilter.java new file mode 100644 index 0000000000..e0b1c1b29c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardAssetTypeFilter.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2026 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.common.data.dashboard.filter; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DashboardAssetTypeFilter extends DashboardAliasFilter { + + @NotEmpty + private List<@NotBlank String> assetTypes; + + private String assetNameFilter; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardDeviceSearchQueryFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardDeviceSearchQueryFilter.java new file mode 100644 index 0000000000..9d9e3a6019 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardDeviceSearchQueryFilter.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2026 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.common.data.dashboard.filter; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DashboardDeviceSearchQueryFilter extends DashboardEntitySearchQueryFilter { + + @NotEmpty + private List<@NotBlank String> deviceTypes; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardDeviceTypeFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardDeviceTypeFilter.java new file mode 100644 index 0000000000..103a164db6 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardDeviceTypeFilter.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2026 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.common.data.dashboard.filter; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DashboardDeviceTypeFilter extends DashboardAliasFilter { + + @NotEmpty + private List<@NotBlank String> deviceTypes; + + private String deviceNameFilter; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardEdgeSearchQueryFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardEdgeSearchQueryFilter.java new file mode 100644 index 0000000000..a30b959f48 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardEdgeSearchQueryFilter.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2026 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.common.data.dashboard.filter; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DashboardEdgeSearchQueryFilter extends DashboardEntitySearchQueryFilter { + + @NotEmpty + private List<@NotBlank String> edgeTypes; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardEdgeTypeFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardEdgeTypeFilter.java new file mode 100644 index 0000000000..95923eec30 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardEdgeTypeFilter.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2026 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.common.data.dashboard.filter; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DashboardEdgeTypeFilter extends DashboardAliasFilter { + + @NotEmpty + private List<@NotBlank String> edgeTypes; + + private String edgeNameFilter; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardEntityListFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardEntityListFilter.java new file mode 100644 index 0000000000..13767ea594 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardEntityListFilter.java @@ -0,0 +1,39 @@ +/** + * Copyright © 2016-2026 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.common.data.dashboard.filter; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.thingsboard.server.common.data.EntityType; + +import java.util.List; +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DashboardEntityListFilter extends DashboardAliasFilter { + + @NotNull + private EntityType entityType; + + @NotEmpty + private List<@NotNull UUID> entityList; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardEntityNameFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardEntityNameFilter.java new file mode 100644 index 0000000000..4286239275 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardEntityNameFilter.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2026 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.common.data.dashboard.filter; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.thingsboard.server.common.data.EntityType; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DashboardEntityNameFilter extends DashboardAliasFilter { + + @NotNull + private EntityType entityType; + + @NotBlank + private String entityNameFilter; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardEntitySearchQueryFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardEntitySearchQueryFilter.java new file mode 100644 index 0000000000..1151b336f0 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardEntitySearchQueryFilter.java @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2026 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.common.data.dashboard.filter; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.thingsboard.server.common.data.query.AliasEntityId; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public abstract class DashboardEntitySearchQueryFilter extends DashboardAliasFilter implements DashboardStatefulRootFilter { + + private AliasEntityId rootEntity; + + private boolean rootStateEntity; + + private String stateEntityParamName; + + private AliasEntityId defaultStateEntity; + + private String relationType; + + @NotNull + private EntitySearchDirection direction; + + @PositiveOrZero + private int maxLevel; + + private boolean fetchLastLevelOnly; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardEntityTypeFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardEntityTypeFilter.java new file mode 100644 index 0000000000..d36f2495ff --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardEntityTypeFilter.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2026 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.common.data.dashboard.filter; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.thingsboard.server.common.data.EntityType; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DashboardEntityTypeFilter extends DashboardAliasFilter { + + @NotNull + private EntityType entityType; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardEntityViewSearchQueryFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardEntityViewSearchQueryFilter.java new file mode 100644 index 0000000000..c28973b6f2 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardEntityViewSearchQueryFilter.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2026 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.common.data.dashboard.filter; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DashboardEntityViewSearchQueryFilter extends DashboardEntitySearchQueryFilter { + + @NotEmpty + private List<@NotBlank String> entityViewTypes; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardEntityViewTypeFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardEntityViewTypeFilter.java new file mode 100644 index 0000000000..0211ce75d2 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardEntityViewTypeFilter.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2026 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.common.data.dashboard.filter; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DashboardEntityViewTypeFilter extends DashboardAliasFilter { + + @NotEmpty + private List<@NotBlank String> entityViewTypes; + + private String entityViewNameFilter; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardRelationsQueryFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardRelationsQueryFilter.java new file mode 100644 index 0000000000..93423d7a11 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardRelationsQueryFilter.java @@ -0,0 +1,54 @@ +/** + * Copyright © 2016-2026 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.common.data.dashboard.filter; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.thingsboard.server.common.data.query.AliasEntityId; +import org.thingsboard.server.common.data.relation.EntitySearchDirection; +import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter; + +import java.util.List; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DashboardRelationsQueryFilter extends DashboardAliasFilter implements DashboardStatefulRootFilter { + + private AliasEntityId rootEntity; + + private boolean rootStateEntity; + + private String stateEntityParamName; + + private AliasEntityId defaultStateEntity; + + @NotNull + private EntitySearchDirection direction; + + @PositiveOrZero + private int maxLevel; + + private boolean fetchLastLevelOnly; + + @Valid + private List filters; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardSingleEntityFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardSingleEntityFilter.java new file mode 100644 index 0000000000..b1fe8aaba2 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardSingleEntityFilter.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2026 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.common.data.dashboard.filter; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.thingsboard.server.common.data.query.AliasEntityId; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DashboardSingleEntityFilter extends DashboardAliasFilter { + + @NotNull + private AliasEntityId singleEntity; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardStateEntityFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardStateEntityFilter.java new file mode 100644 index 0000000000..b6cbe08973 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardStateEntityFilter.java @@ -0,0 +1,32 @@ +/** + * Copyright © 2016-2026 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.common.data.dashboard.filter; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.thingsboard.server.common.data.query.AliasEntityId; + +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DashboardStateEntityFilter extends DashboardAliasFilter { + + private String stateEntityParamName; + + private AliasEntityId defaultStateEntity; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardStatefulRootFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardStatefulRootFilter.java new file mode 100644 index 0000000000..709ee18c76 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/dashboard/filter/DashboardStatefulRootFilter.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2026 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.common.data.dashboard.filter; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.validation.constraints.AssertTrue; +import org.thingsboard.server.common.data.query.AliasEntityId; + +public interface DashboardStatefulRootFilter { + + AliasEntityId getRootEntity(); + + boolean isRootStateEntity(); + + @AssertTrue(message = "must include 'rootEntity' when 'rootStateEntity' is false") + @JsonIgnore + default boolean isValidRootEntity() { + return isRootStateEntity() || getRootEntity() != null; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java index 5df895ac80..00b240dc01 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java @@ -61,6 +61,10 @@ public class ConstraintValidator { } } + public static Set> getViolations(T data) { + return fieldsValidator.validate(data); + } + public static String getErrorMessage(Collection> constraintsViolations) { return constraintsViolations.stream() .map(ConstraintValidator::getErrorMessage) diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DashboardDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DashboardDataValidator.java index 3236b75b2c..dcf0560f09 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/DashboardDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/DashboardDataValidator.java @@ -15,20 +15,33 @@ */ package org.thingsboard.server.dao.service.validator; -import org.springframework.beans.factory.annotation.Autowired; +import com.fasterxml.jackson.databind.JsonNode; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ElementKind; +import jakarta.validation.Path; +import jakarta.validation.constraints.AssertTrue; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.dashboard.DashboardConfig; +import org.thingsboard.server.common.data.dashboard.EntityAlias; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.tenant.TenantService; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + @Component +@RequiredArgsConstructor public class DashboardDataValidator extends DataValidator { - @Autowired - private TenantService tenantService; + private final TenantService tenantService; @Override protected void validateCreate(TenantId tenantId, Dashboard data) { @@ -45,5 +58,74 @@ public class DashboardDataValidator extends DataValidator { throw new DataValidationException("Dashboard is referencing to non-existent tenant!"); } } + if (dashboard.getConfiguration() == null || !dashboard.getConfiguration().isObject()) { + return; + } + JsonNode aliasesNode = dashboard.getConfiguration().get("entityAliases"); + if (aliasesNode == null || aliasesNode.isNull()) { + return; + } + DashboardConfig parsed; + try { + parsed = JacksonUtil.OBJECT_MAPPER.convertValue(dashboard.getConfiguration(), DashboardConfig.class); + } catch (IllegalArgumentException e) { + throw new DataValidationException("Dashboard configuration has invalid structure: " + e.getMessage()); + } + validateEntityAliases(parsed); + } + + private static void validateEntityAliases(DashboardConfig config) { + if (config.getEntityAliases() == null) { + return; + } + Set> violations = ConstraintValidator.getViolations(config); + if (!violations.isEmpty()) { + throw new DataValidationException(violations.stream() + .map(v -> formatViolation(v, config)) + .distinct() + .sorted() + .collect(Collectors.joining(", ", "Dashboard validation error: ", ""))); + } + config.getEntityAliases().forEach(DashboardDataValidator::validateEntityAliasKey); + } + + private static String formatViolation(ConstraintViolation v, DashboardConfig cfg) { + UUID aliasKey = null; + String fieldName = null; + Integer elementIndex = null; + for (Path.Node node : v.getPropertyPath()) { + if (node.getKey() instanceof UUID uuid) { + aliasKey = uuid; + } + if (node.getKind() == ElementKind.CONTAINER_ELEMENT) { + if (node.getIndex() != null) { + elementIndex = node.getIndex(); + } + } else if (node.getName() != null) { + fieldName = node.getName(); + } + } + String elementInfo = elementIndex != null ? " element at index " + elementIndex : ""; + boolean isLogicConstraint = v.getConstraintDescriptor().getAnnotation().annotationType().equals(AssertTrue.class); + if (aliasKey != null) { + EntityAlias alias = cfg.getEntityAliases().get(aliasKey); + String aliasName = alias != null && alias.getAlias() != null && !alias.getAlias().isBlank() + ? alias.getAlias() : aliasKey.toString(); + if (isLogicConstraint) { + return "alias '" + aliasName + "' " + v.getMessage(); + } + return "alias '" + aliasName + "' field '" + fieldName + "'" + elementInfo + " " + v.getMessage(); + } + if (isLogicConstraint) { + return v.getMessage(); + } + return "field '" + fieldName + "'" + elementInfo + " " + v.getMessage(); } + + private static void validateEntityAliasKey(UUID key, EntityAlias alias) { + if (!alias.getId().equals(key)) { + throw new DataValidationException("Dashboard validation error: alias '" + alias.getAlias() + "' has 'id' that does not match its key!"); + } + } + } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/AbstractDashboardDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/AbstractDashboardDataValidatorTest.java new file mode 100644 index 0000000000..ac912be352 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/AbstractDashboardDataValidatorTest.java @@ -0,0 +1,75 @@ +/** + * Copyright © 2016-2026 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.dao.service.validator; + +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.tenant.TenantService; + +import java.util.UUID; + +import static org.mockito.BDDMockito.willReturn; + +@SpringBootTest(classes = DashboardDataValidator.class) +public abstract class AbstractDashboardDataValidatorTest { + + protected static final String ALIAS_UUID = "a1ddb8fa-90ff-5598-e7f2-e254194d055d"; + + @MockitoBean + protected TenantService tenantService; + @Autowired + protected DashboardDataValidator validator; + + protected final TenantId tenantId = TenantId.fromUUID(UUID.fromString("9ef79cdf-37a8-4119-b682-2e7ed4e018da")); + + @BeforeEach + void setUp() { + willReturn(true).given(tenantService).tenantExists(tenantId); + } + + protected Dashboard dashboardWith(JsonNode configuration) { + Dashboard dashboard = new Dashboard(); + dashboard.setTitle("test dashboard"); + dashboard.setTenantId(tenantId); + dashboard.setConfiguration(configuration); + return dashboard; + } + + protected Dashboard filterAlias(String aliasName, String filterJson) { + String json = """ + { + "entityAliases": { + "%s": { + "id": "%s", + "alias": "%s", + "filter": %s + } + } + }""".formatted(ALIAS_UUID, ALIAS_UUID, aliasName, filterJson); + return dashboardWith(JacksonUtil.toJsonNode(json)); + } + + protected void validate(Dashboard dashboard) { + validator.validateDataImpl(tenantId, dashboard); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/DashboardDataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/DashboardDataValidatorTest.java deleted file mode 100644 index 3029027f37..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/service/validator/DashboardDataValidatorTest.java +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Copyright © 2016-2026 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.dao.service.validator; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.test.mock.mockito.SpyBean; -import org.thingsboard.server.common.data.Dashboard; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.dao.tenant.TenantService; - -import java.util.UUID; - -import static org.mockito.BDDMockito.willReturn; -import static org.mockito.Mockito.verify; - -@SpringBootTest(classes = DashboardDataValidator.class) -class DashboardDataValidatorTest { - - @MockBean - TenantService tenantService; - @SpyBean - DashboardDataValidator validator; - TenantId tenantId = TenantId.fromUUID(UUID.fromString("9ef79cdf-37a8-4119-b682-2e7ed4e018da")); - - @BeforeEach - void setUp() { - willReturn(true).given(tenantService).tenantExists(tenantId); - } - - @Test - void testValidateNameInvocation() { - Dashboard dashboard = new Dashboard(); - dashboard.setTitle("flight control"); - dashboard.setTenantId(tenantId); - - validator.validateDataImpl(tenantId, dashboard); - verify(validator).validateString("Dashboard title", dashboard.getTitle()); - } - -} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardApiUsageStateFilterValidationTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardApiUsageStateFilterValidationTest.java new file mode 100644 index 0000000000..16dc74d03a --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardApiUsageStateFilterValidationTest.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2026 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.dao.service.validator.dashboard; + +import org.thingsboard.server.dao.service.validator.AbstractDashboardDataValidatorTest; + +import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.Dashboard; + +import static org.assertj.core.api.Assertions.assertThatCode; + +class DashboardApiUsageStateFilterValidationTest extends AbstractDashboardDataValidatorTest { + + @Test + void shouldAcceptApiUsageStateFilterWithoutCustomerId() { + Dashboard dashboard = filterAlias("Tenant Usage", """ + {"type": "apiUsageState"}"""); + + assertThatCode(() -> validate(dashboard)).doesNotThrowAnyException(); + } + + @Test + void shouldAcceptApiUsageStateFilterWithCustomerId() { + Dashboard dashboard = filterAlias("Customer Usage", """ + {"type": "apiUsageState", "customerId": {"entityType": "CUSTOMER", "id": "%s"}}""".formatted(ALIAS_UUID)); + + assertThatCode(() -> validate(dashboard)).doesNotThrowAnyException(); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardAssetSearchQueryFilterValidationTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardAssetSearchQueryFilterValidationTest.java new file mode 100644 index 0000000000..70f81d63b4 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardAssetSearchQueryFilterValidationTest.java @@ -0,0 +1,75 @@ +/** + * Copyright © 2016-2026 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.dao.service.validator.dashboard; + +import org.thingsboard.server.dao.service.validator.AbstractDashboardDataValidatorTest; + +import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.dao.exception.DataValidationException; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class DashboardAssetSearchQueryFilterValidationTest extends AbstractDashboardDataValidatorTest { + + @Test + void shouldAcceptValidAssetSearchQueryFilter() { + Dashboard dashboard = filterAlias("Assets", """ + { + "type": "assetSearchQuery", + "rootEntity": {"entityType": "DEVICE", "id": "%s"}, + "direction": "FROM", + "maxLevel": 1, + "assetTypes": ["thermostat"] + }""".formatted(ALIAS_UUID)); + + assertThatCode(() -> validate(dashboard)).doesNotThrowAnyException(); + } + + @Test + void shouldRejectAssetSearchQueryFilterWithEmptyAssetTypes() { + Dashboard dashboard = filterAlias("Assets", """ + { + "type": "assetSearchQuery", + "rootEntity": {"entityType": "DEVICE", "id": "%s"}, + "direction": "FROM", + "maxLevel": 1, + "assetTypes": [] + }""".formatted(ALIAS_UUID)); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias 'Assets' field 'assetTypes' must not be empty"); + } + + @Test + void shouldRejectAssetSearchQueryFilterWithBlankAssetTypeElement() { + Dashboard dashboard = filterAlias("Assets", """ + { + "type": "assetSearchQuery", + "rootEntity": {"entityType": "DEVICE", "id": "%s"}, + "direction": "FROM", + "maxLevel": 1, + "assetTypes": ["thermostat", " "] + }""".formatted(ALIAS_UUID)); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias 'Assets' field 'assetTypes' element at index 1 must not be blank"); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardAssetTypeFilterValidationTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardAssetTypeFilterValidationTest.java new file mode 100644 index 0000000000..53a9b7bfb0 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardAssetTypeFilterValidationTest.java @@ -0,0 +1,57 @@ +/** + * Copyright © 2016-2026 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.dao.service.validator.dashboard; + +import org.thingsboard.server.dao.service.validator.AbstractDashboardDataValidatorTest; + +import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.dao.exception.DataValidationException; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class DashboardAssetTypeFilterValidationTest extends AbstractDashboardDataValidatorTest { + + @Test + void shouldAcceptValidAssetTypeFilter() { + Dashboard dashboard = filterAlias("Assets", """ + {"type": "assetType", "assetTypes": ["thermostat", "valve"]}"""); + + assertThatCode(() -> validate(dashboard)).doesNotThrowAnyException(); + } + + @Test + void shouldRejectAssetTypeFilterWithEmptyAssetTypes() { + Dashboard dashboard = filterAlias("Assets", """ + {"type": "assetType", "assetTypes": []}"""); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias 'Assets' field 'assetTypes' must not be empty"); + } + + @Test + void shouldRejectAssetTypeFilterWithBlankAssetTypeElement() { + Dashboard dashboard = filterAlias("Assets", """ + {"type": "assetType", "assetTypes": ["thermostat", " "]}"""); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias 'Assets' field 'assetTypes' element at index 1 must not be blank"); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardConfigurationStructureTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardConfigurationStructureTest.java new file mode 100644 index 0000000000..e0cda63061 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardConfigurationStructureTest.java @@ -0,0 +1,314 @@ +/** + * Copyright © 2016-2026 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.dao.service.validator.dashboard; + +import org.thingsboard.server.dao.service.validator.AbstractDashboardDataValidatorTest; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.dao.exception.DataValidationException; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class DashboardConfigurationStructureTest extends AbstractDashboardDataValidatorTest { + + @Nested + class Title { + + @Test + void shouldAcceptValidTitle() { + Dashboard dashboard = new Dashboard(); + dashboard.setTitle("flight control"); + dashboard.setTenantId(tenantId); + + assertThatCode(() -> validate(dashboard)).doesNotThrowAnyException(); + } + + @Test + void shouldRejectBlankTitle() { + Dashboard dashboard = new Dashboard(); + dashboard.setTitle(" "); + dashboard.setTenantId(tenantId); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard title should be specified!"); + } + + } + + @Nested + class Tenant { + + @Test + void shouldRejectDashboardReferencingNonExistentTenant() { + TenantId unknownTenantId = TenantId.fromUUID(UUID.fromString("11111111-1111-1111-1111-111111111111")); + Dashboard dashboard = new Dashboard(); + dashboard.setTitle("flight control"); + dashboard.setTenantId(unknownTenantId); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard is referencing to non-existent tenant!"); + } + + } + + @Nested + class ConfigurationShape { + + @Test + void shouldAcceptDashboardWithoutConfiguration() { + Dashboard dashboard = dashboardWith(null); + + assertThatCode(() -> validate(dashboard)).doesNotThrowAnyException(); + } + + @Test + void shouldAcceptNonObjectConfiguration() { + Dashboard dashboard = dashboardWith(JacksonUtil.toJsonNode("\"just a string\"")); + + assertThatCode(() -> validate(dashboard)).doesNotThrowAnyException(); + } + + @Test + void shouldAcceptConfigurationWithoutEntityAliases() { + Dashboard dashboard = dashboardWith(JacksonUtil.toJsonNode("{\"widgets\": {}}")); + + assertThatCode(() -> validate(dashboard)).doesNotThrowAnyException(); + } + + @Test + void shouldAcceptEmptyEntityAliasesMap() { + Dashboard dashboard = dashboardWith(JacksonUtil.toJsonNode("{\"entityAliases\": {}}")); + + assertThatCode(() -> validate(dashboard)).doesNotThrowAnyException(); + } + + @Test + void shouldAcceptNullEntityAliases() { + Dashboard dashboard = dashboardWith(JacksonUtil.toJsonNode("{\"entityAliases\": null}")); + + assertThatCode(() -> validate(dashboard)).doesNotThrowAnyException(); + } + + @Test + void shouldRejectEntityAliasesAsArray() { + Dashboard dashboard = dashboardWith(JacksonUtil.toJsonNode("{\"entityAliases\": []}")); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessageStartingWith("Dashboard configuration has invalid structure:"); + } + + } + + @Nested + class EntityAliasShape { + + @Test + void shouldAcceptWellFormedEntityAlias() { + Dashboard dashboard = filterAlias("Devices", """ + {"type": "entityType", "entityType": "DEVICE"}"""); + + assertThatCode(() -> validate(dashboard)).doesNotThrowAnyException(); + } + + @Test + void shouldRejectBlankAliasDisplayName() { + String json = """ + { + "entityAliases": { + "%s": { + "id": "%s", + "alias": "", + "filter": {"type": "entityType", "entityType": "DEVICE"} + } + } + }""".formatted(ALIAS_UUID, ALIAS_UUID); + Dashboard dashboard = dashboardWith(JacksonUtil.toJsonNode(json)); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias '" + ALIAS_UUID + "' field 'alias' must not be blank"); + } + + @Test + void shouldRejectKeyThatIsNotUuid() { + String json = """ + {"entityAliases": {"not-a-uuid": {"id": "%s", "alias": "X", "filter": {"type": "entityType", "entityType": "DEVICE"}}}}""".formatted(ALIAS_UUID); + Dashboard dashboard = dashboardWith(JacksonUtil.toJsonNode(json)); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessageStartingWith("Dashboard configuration has invalid structure:"); + } + + @Test + void shouldRejectAliasValueThatIsNotObject() { + String json = """ + {"entityAliases": {"%s": "not an object"}}""".formatted(ALIAS_UUID); + Dashboard dashboard = dashboardWith(JacksonUtil.toJsonNode(json)); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessageStartingWith("Dashboard configuration has invalid structure:"); + } + + @Test + void shouldRejectMissingIdField() { + String json = """ + { + "entityAliases": { + "%s": {"alias": "Devices", "filter": {"type": "entityType", "entityType": "DEVICE"}} + } + }""".formatted(ALIAS_UUID); + Dashboard dashboard = dashboardWith(JacksonUtil.toJsonNode(json)); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias 'Devices' field 'id' must not be null"); + } + + @Test + void shouldRejectNonTextualIdField() { + String json = """ + { + "entityAliases": { + "%s": {"id": 42, "alias": "Devices", "filter": {"type": "entityType", "entityType": "DEVICE"}} + } + }""".formatted(ALIAS_UUID); + Dashboard dashboard = dashboardWith(JacksonUtil.toJsonNode(json)); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessageStartingWith("Dashboard configuration has invalid structure:"); + } + + @Test + void shouldRejectIdThatIsNotUuid() { + String json = """ + { + "entityAliases": { + "%s": {"id": "not-a-uuid", "alias": "Devices", "filter": {"type": "entityType", "entityType": "DEVICE"}} + } + }""".formatted(ALIAS_UUID); + Dashboard dashboard = dashboardWith(JacksonUtil.toJsonNode(json)); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessageStartingWith("Dashboard configuration has invalid structure:"); + } + + @Test + void shouldRejectIdNotMatchingKey() { + String json = """ + { + "entityAliases": { + "%s": {"id": "11111111-1111-1111-1111-111111111111", "alias": "Devices", "filter": {"type": "entityType", "entityType": "DEVICE"}} + } + }""".formatted(ALIAS_UUID); + Dashboard dashboard = dashboardWith(JacksonUtil.toJsonNode(json)); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias 'Devices' has 'id' that does not match its key!"); + } + + @Test + void shouldRejectMissingAliasField() { + String json = """ + { + "entityAliases": { + "%s": {"id": "%s", "filter": {"type": "entityType", "entityType": "DEVICE"}} + } + }""".formatted(ALIAS_UUID, ALIAS_UUID); + Dashboard dashboard = dashboardWith(JacksonUtil.toJsonNode(json)); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias '" + ALIAS_UUID + "' field 'alias' must not be blank"); + } + + @Test + void shouldRejectMissingFilterField() { + String json = """ + { + "entityAliases": { + "%s": {"id": "%s", "alias": "Devices"} + } + }""".formatted(ALIAS_UUID, ALIAS_UUID); + Dashboard dashboard = dashboardWith(JacksonUtil.toJsonNode(json)); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias 'Devices' field 'filter' must not be null"); + } + + @Test + void shouldRejectFilterWithoutType() { + String json = """ + { + "entityAliases": { + "%s": {"id": "%s", "alias": "Devices", "filter": {}} + } + }""".formatted(ALIAS_UUID, ALIAS_UUID); + Dashboard dashboard = dashboardWith(JacksonUtil.toJsonNode(json)); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessageStartingWith("Dashboard configuration has invalid structure:"); + } + + @Test + void shouldRejectFilterWithUnknownType() { + String json = """ + { + "entityAliases": { + "%s": {"id": "%s", "alias": "Devices", "filter": {"type": "bogus"}} + } + }""".formatted(ALIAS_UUID, ALIAS_UUID); + Dashboard dashboard = dashboardWith(JacksonUtil.toJsonNode(json)); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessageStartingWith("Dashboard configuration has invalid structure:"); + } + + @Test + void shouldRejectNonObjectFilterField() { + String json = """ + { + "entityAliases": { + "%s": {"id": "%s", "alias": "Devices", "filter": "not an object"} + } + }""".formatted(ALIAS_UUID, ALIAS_UUID); + Dashboard dashboard = dashboardWith(JacksonUtil.toJsonNode(json)); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessageStartingWith("Dashboard configuration has invalid structure:"); + } + + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardDeviceSearchQueryFilterValidationTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardDeviceSearchQueryFilterValidationTest.java new file mode 100644 index 0000000000..4bb2629166 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardDeviceSearchQueryFilterValidationTest.java @@ -0,0 +1,75 @@ +/** + * Copyright © 2016-2026 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.dao.service.validator.dashboard; + +import org.thingsboard.server.dao.service.validator.AbstractDashboardDataValidatorTest; + +import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.dao.exception.DataValidationException; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class DashboardDeviceSearchQueryFilterValidationTest extends AbstractDashboardDataValidatorTest { + + @Test + void shouldAcceptValidDeviceSearchQueryFilter() { + Dashboard dashboard = filterAlias("Devices", """ + { + "type": "deviceSearchQuery", + "rootEntity": {"entityType": "ASSET", "id": "%s"}, + "direction": "FROM", + "maxLevel": 1, + "deviceTypes": ["thermostat"] + }""".formatted(ALIAS_UUID)); + + assertThatCode(() -> validate(dashboard)).doesNotThrowAnyException(); + } + + @Test + void shouldRejectDeviceSearchQueryFilterWithEmptyDeviceTypes() { + Dashboard dashboard = filterAlias("Devices", """ + { + "type": "deviceSearchQuery", + "rootEntity": {"entityType": "ASSET", "id": "%s"}, + "direction": "FROM", + "maxLevel": 1, + "deviceTypes": [] + }""".formatted(ALIAS_UUID)); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias 'Devices' field 'deviceTypes' must not be empty"); + } + + @Test + void shouldRejectDeviceSearchQueryFilterWithBlankDeviceTypeElement() { + Dashboard dashboard = filterAlias("Devices", """ + { + "type": "deviceSearchQuery", + "rootEntity": {"entityType": "ASSET", "id": "%s"}, + "direction": "FROM", + "maxLevel": 1, + "deviceTypes": ["thermostat", " "] + }""".formatted(ALIAS_UUID)); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias 'Devices' field 'deviceTypes' element at index 1 must not be blank"); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardDeviceTypeFilterValidationTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardDeviceTypeFilterValidationTest.java new file mode 100644 index 0000000000..0552a9ee44 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardDeviceTypeFilterValidationTest.java @@ -0,0 +1,57 @@ +/** + * Copyright © 2016-2026 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.dao.service.validator.dashboard; + +import org.thingsboard.server.dao.service.validator.AbstractDashboardDataValidatorTest; + +import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.dao.exception.DataValidationException; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class DashboardDeviceTypeFilterValidationTest extends AbstractDashboardDataValidatorTest { + + @Test + void shouldAcceptValidDeviceTypeFilter() { + Dashboard dashboard = filterAlias("Devices", """ + {"type": "deviceType", "deviceTypes": ["thermostat", "valve"]}"""); + + assertThatCode(() -> validate(dashboard)).doesNotThrowAnyException(); + } + + @Test + void shouldRejectDeviceTypeFilterWithEmptyDeviceTypes() { + Dashboard dashboard = filterAlias("Devices", """ + {"type": "deviceType", "deviceTypes": []}"""); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias 'Devices' field 'deviceTypes' must not be empty"); + } + + @Test + void shouldRejectDeviceTypeFilterWithBlankDeviceTypeElement() { + Dashboard dashboard = filterAlias("Devices", """ + {"type": "deviceType", "deviceTypes": ["thermostat", " "]}"""); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias 'Devices' field 'deviceTypes' element at index 1 must not be blank"); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardEdgeSearchQueryFilterValidationTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardEdgeSearchQueryFilterValidationTest.java new file mode 100644 index 0000000000..e7c8928195 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardEdgeSearchQueryFilterValidationTest.java @@ -0,0 +1,75 @@ +/** + * Copyright © 2016-2026 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.dao.service.validator.dashboard; + +import org.thingsboard.server.dao.service.validator.AbstractDashboardDataValidatorTest; + +import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.dao.exception.DataValidationException; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class DashboardEdgeSearchQueryFilterValidationTest extends AbstractDashboardDataValidatorTest { + + @Test + void shouldAcceptValidEdgeSearchQueryFilter() { + Dashboard dashboard = filterAlias("Edges", """ + { + "type": "edgeSearchQuery", + "rootEntity": {"entityType": "TENANT", "id": "%s"}, + "direction": "FROM", + "maxLevel": 1, + "edgeTypes": ["gateway"] + }""".formatted(ALIAS_UUID)); + + assertThatCode(() -> validate(dashboard)).doesNotThrowAnyException(); + } + + @Test + void shouldRejectEdgeSearchQueryFilterWithEmptyEdgeTypes() { + Dashboard dashboard = filterAlias("Edges", """ + { + "type": "edgeSearchQuery", + "rootEntity": {"entityType": "TENANT", "id": "%s"}, + "direction": "FROM", + "maxLevel": 1, + "edgeTypes": [] + }""".formatted(ALIAS_UUID)); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias 'Edges' field 'edgeTypes' must not be empty"); + } + + @Test + void shouldRejectEdgeSearchQueryFilterWithBlankEdgeTypeElement() { + Dashboard dashboard = filterAlias("Edges", """ + { + "type": "edgeSearchQuery", + "rootEntity": {"entityType": "TENANT", "id": "%s"}, + "direction": "FROM", + "maxLevel": 1, + "edgeTypes": ["gateway", " "] + }""".formatted(ALIAS_UUID)); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias 'Edges' field 'edgeTypes' element at index 1 must not be blank"); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardEdgeTypeFilterValidationTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardEdgeTypeFilterValidationTest.java new file mode 100644 index 0000000000..a7a10366c6 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardEdgeTypeFilterValidationTest.java @@ -0,0 +1,57 @@ +/** + * Copyright © 2016-2026 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.dao.service.validator.dashboard; + +import org.thingsboard.server.dao.service.validator.AbstractDashboardDataValidatorTest; + +import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.dao.exception.DataValidationException; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class DashboardEdgeTypeFilterValidationTest extends AbstractDashboardDataValidatorTest { + + @Test + void shouldAcceptValidEdgeTypeFilter() { + Dashboard dashboard = filterAlias("Edges", """ + {"type": "edgeType", "edgeTypes": ["gateway"]}"""); + + assertThatCode(() -> validate(dashboard)).doesNotThrowAnyException(); + } + + @Test + void shouldRejectEdgeTypeFilterWithEmptyEdgeTypes() { + Dashboard dashboard = filterAlias("Edges", """ + {"type": "edgeType", "edgeTypes": []}"""); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias 'Edges' field 'edgeTypes' must not be empty"); + } + + @Test + void shouldRejectEdgeTypeFilterWithBlankEdgeTypeElement() { + Dashboard dashboard = filterAlias("Edges", """ + {"type": "edgeType", "edgeTypes": ["gateway", " "]}"""); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias 'Edges' field 'edgeTypes' element at index 1 must not be blank"); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardEntityListFilterValidationTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardEntityListFilterValidationTest.java new file mode 100644 index 0000000000..9457173cf3 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardEntityListFilterValidationTest.java @@ -0,0 +1,57 @@ +/** + * Copyright © 2016-2026 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.dao.service.validator.dashboard; + +import org.thingsboard.server.dao.service.validator.AbstractDashboardDataValidatorTest; + +import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.dao.exception.DataValidationException; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class DashboardEntityListFilterValidationTest extends AbstractDashboardDataValidatorTest { + + @Test + void shouldAcceptValidEntityListFilter() { + Dashboard dashboard = filterAlias("Devices", """ + {"type": "entityList", "entityType": "DEVICE", "entityList": ["11111111-1111-1111-1111-111111111111"]}"""); + + assertThatCode(() -> validate(dashboard)).doesNotThrowAnyException(); + } + + @Test + void shouldRejectEntityListFilterWithEmptyEntityList() { + Dashboard dashboard = filterAlias("Devices", """ + {"type": "entityList", "entityType": "DEVICE", "entityList": []}"""); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias 'Devices' field 'entityList' must not be empty"); + } + + @Test + void shouldRejectEntityListFilterWithoutEntityType() { + Dashboard dashboard = filterAlias("Devices", """ + {"type": "entityList", "entityList": ["11111111-1111-1111-1111-111111111111"]}"""); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias 'Devices' field 'entityType' must not be null"); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardEntityNameFilterValidationTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardEntityNameFilterValidationTest.java new file mode 100644 index 0000000000..948ac3aa23 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardEntityNameFilterValidationTest.java @@ -0,0 +1,57 @@ +/** + * Copyright © 2016-2026 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.dao.service.validator.dashboard; + +import org.thingsboard.server.dao.service.validator.AbstractDashboardDataValidatorTest; + +import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.dao.exception.DataValidationException; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class DashboardEntityNameFilterValidationTest extends AbstractDashboardDataValidatorTest { + + @Test + void shouldAcceptValidEntityNameFilter() { + Dashboard dashboard = filterAlias("Sensors", """ + {"type": "entityName", "entityType": "DEVICE", "entityNameFilter": "sensor"}"""); + + assertThatCode(() -> validate(dashboard)).doesNotThrowAnyException(); + } + + @Test + void shouldRejectEntityNameFilterWithoutEntityType() { + Dashboard dashboard = filterAlias("Sensors", """ + {"type": "entityName", "entityNameFilter": "sensor"}"""); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias 'Sensors' field 'entityType' must not be null"); + } + + @Test + void shouldRejectEntityNameFilterWithBlankEntityNameFilter() { + Dashboard dashboard = filterAlias("Sensors", """ + {"type": "entityName", "entityType": "DEVICE", "entityNameFilter": " "}"""); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias 'Sensors' field 'entityNameFilter' must not be blank"); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardEntityTypeFilterValidationTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardEntityTypeFilterValidationTest.java new file mode 100644 index 0000000000..7fe46f6036 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardEntityTypeFilterValidationTest.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2026 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.dao.service.validator.dashboard; + +import org.thingsboard.server.dao.service.validator.AbstractDashboardDataValidatorTest; + +import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.dao.exception.DataValidationException; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class DashboardEntityTypeFilterValidationTest extends AbstractDashboardDataValidatorTest { + + @Test + void shouldAcceptValidEntityTypeFilter() { + Dashboard dashboard = filterAlias("Devices", """ + {"type": "entityType", "entityType": "DEVICE"}"""); + + assertThatCode(() -> validate(dashboard)).doesNotThrowAnyException(); + } + + @Test + void shouldRejectEntityTypeFilterWithoutEntityType() { + Dashboard dashboard = filterAlias("Devices", """ + {"type": "entityType"}"""); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias 'Devices' field 'entityType' must not be null"); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardEntityViewSearchQueryFilterValidationTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardEntityViewSearchQueryFilterValidationTest.java new file mode 100644 index 0000000000..e92a83e1ec --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardEntityViewSearchQueryFilterValidationTest.java @@ -0,0 +1,75 @@ +/** + * Copyright © 2016-2026 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.dao.service.validator.dashboard; + +import org.thingsboard.server.dao.service.validator.AbstractDashboardDataValidatorTest; + +import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.dao.exception.DataValidationException; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class DashboardEntityViewSearchQueryFilterValidationTest extends AbstractDashboardDataValidatorTest { + + @Test + void shouldAcceptValidEntityViewSearchQueryFilter() { + Dashboard dashboard = filterAlias("Views", """ + { + "type": "entityViewSearchQuery", + "rootEntity": {"entityType": "DEVICE", "id": "%s"}, + "direction": "FROM", + "maxLevel": 1, + "entityViewTypes": ["summary"] + }""".formatted(ALIAS_UUID)); + + assertThatCode(() -> validate(dashboard)).doesNotThrowAnyException(); + } + + @Test + void shouldRejectEntityViewSearchQueryFilterWithEmptyEntityViewTypes() { + Dashboard dashboard = filterAlias("Views", """ + { + "type": "entityViewSearchQuery", + "rootEntity": {"entityType": "DEVICE", "id": "%s"}, + "direction": "FROM", + "maxLevel": 1, + "entityViewTypes": [] + }""".formatted(ALIAS_UUID)); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias 'Views' field 'entityViewTypes' must not be empty"); + } + + @Test + void shouldRejectEntityViewSearchQueryFilterWithBlankEntityViewTypeElement() { + Dashboard dashboard = filterAlias("Views", """ + { + "type": "entityViewSearchQuery", + "rootEntity": {"entityType": "DEVICE", "id": "%s"}, + "direction": "FROM", + "maxLevel": 1, + "entityViewTypes": ["summary", " "] + }""".formatted(ALIAS_UUID)); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias 'Views' field 'entityViewTypes' element at index 1 must not be blank"); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardEntityViewTypeFilterValidationTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardEntityViewTypeFilterValidationTest.java new file mode 100644 index 0000000000..818626b0db --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardEntityViewTypeFilterValidationTest.java @@ -0,0 +1,57 @@ +/** + * Copyright © 2016-2026 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.dao.service.validator.dashboard; + +import org.thingsboard.server.dao.service.validator.AbstractDashboardDataValidatorTest; + +import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.dao.exception.DataValidationException; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class DashboardEntityViewTypeFilterValidationTest extends AbstractDashboardDataValidatorTest { + + @Test + void shouldAcceptValidEntityViewTypeFilter() { + Dashboard dashboard = filterAlias("Views", """ + {"type": "entityViewType", "entityViewTypes": ["summary"]}"""); + + assertThatCode(() -> validate(dashboard)).doesNotThrowAnyException(); + } + + @Test + void shouldRejectEntityViewTypeFilterWithEmptyEntityViewTypes() { + Dashboard dashboard = filterAlias("Views", """ + {"type": "entityViewType", "entityViewTypes": []}"""); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias 'Views' field 'entityViewTypes' must not be empty"); + } + + @Test + void shouldRejectEntityViewTypeFilterWithBlankEntityViewTypeElement() { + Dashboard dashboard = filterAlias("Views", """ + {"type": "entityViewType", "entityViewTypes": ["summary", " "]}"""); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias 'Views' field 'entityViewTypes' element at index 1 must not be blank"); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardRelationsQueryFilterValidationTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardRelationsQueryFilterValidationTest.java new file mode 100644 index 0000000000..e1ffe299a2 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardRelationsQueryFilterValidationTest.java @@ -0,0 +1,98 @@ +/** + * Copyright © 2016-2026 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.dao.service.validator.dashboard; + +import org.thingsboard.server.dao.service.validator.AbstractDashboardDataValidatorTest; + +import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.dao.exception.DataValidationException; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class DashboardRelationsQueryFilterValidationTest extends AbstractDashboardDataValidatorTest { + + @Test + void shouldAcceptValidRelationsQueryFilter() { + Dashboard dashboard = filterAlias("Related", """ + { + "type": "relationsQuery", + "rootEntity": {"entityType": "DEVICE", "id": "%s"}, + "direction": "FROM", + "maxLevel": 1, + "filters": [] + }""".formatted(ALIAS_UUID)); + + assertThatCode(() -> validate(dashboard)).doesNotThrowAnyException(); + } + + @Test + void shouldAcceptRelationsQueryFilterWithStateDrivenRoot() { + Dashboard dashboard = filterAlias("Related", """ + { + "type": "relationsQuery", + "rootStateEntity": true, + "direction": "FROM", + "maxLevel": 1 + }"""); + + assertThatCode(() -> validate(dashboard)).doesNotThrowAnyException(); + } + + @Test + void shouldAcceptRelationsQueryFilterWithZeroMaxLevel() { + // maxLevel = 0 means "unlimited" in the UI + Dashboard dashboard = filterAlias("Related", """ + { + "type": "relationsQuery", + "rootEntity": {"entityType": "DEVICE", "id": "%s"}, + "direction": "FROM", + "maxLevel": 0 + }""".formatted(ALIAS_UUID)); + + assertThatCode(() -> validate(dashboard)).doesNotThrowAnyException(); + } + + @Test + void shouldRejectRelationsQueryFilterWithoutDirection() { + Dashboard dashboard = filterAlias("Related", """ + { + "type": "relationsQuery", + "rootEntity": {"entityType": "DEVICE", "id": "%s"}, + "maxLevel": 1 + }""".formatted(ALIAS_UUID)); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias 'Related' field 'direction' must not be null"); + } + + @Test + void shouldRejectRelationsQueryFilterWithoutRootEntityWhenStateDrivenIsFalse() { + Dashboard dashboard = filterAlias("Related", """ + { + "type": "relationsQuery", + "direction": "FROM", + "maxLevel": 1 + }"""); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias 'Related' must include 'rootEntity' when 'rootStateEntity' is false"); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardSingleEntityFilterValidationTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardSingleEntityFilterValidationTest.java new file mode 100644 index 0000000000..8d655333e3 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardSingleEntityFilterValidationTest.java @@ -0,0 +1,55 @@ +/** + * Copyright © 2016-2026 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.dao.service.validator.dashboard; + +import org.thingsboard.server.dao.service.validator.AbstractDashboardDataValidatorTest; + +import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.dao.exception.DataValidationException; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class DashboardSingleEntityFilterValidationTest extends AbstractDashboardDataValidatorTest { + + @Test + void shouldAcceptValidSingleEntityFilter() { + Dashboard dashboard = filterAlias("Sensor", """ + {"type": "singleEntity", "singleEntity": {"entityType": "DEVICE", "id": "%s"}}""".formatted(ALIAS_UUID)); + + assertThatCode(() -> validate(dashboard)).doesNotThrowAnyException(); + } + + @Test + void shouldAcceptSingleEntityFilterWithAliasEntity() { + Dashboard dashboard = filterAlias("Current Tenant", """ + {"type": "singleEntity", "singleEntity": {"entityType": "CURRENT_TENANT"}}"""); + + assertThatCode(() -> validate(dashboard)).doesNotThrowAnyException(); + } + + @Test + void shouldRejectSingleEntityFilterWithoutSingleEntity() { + Dashboard dashboard = filterAlias("Sensor", """ + {"type": "singleEntity"}"""); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias 'Sensor' field 'singleEntity' must not be null"); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardStateEntityFilterValidationTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardStateEntityFilterValidationTest.java new file mode 100644 index 0000000000..2c6336b50c --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardStateEntityFilterValidationTest.java @@ -0,0 +1,47 @@ +/** + * Copyright © 2016-2026 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.dao.service.validator.dashboard; + +import org.thingsboard.server.dao.service.validator.AbstractDashboardDataValidatorTest; + +import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.Dashboard; + +import static org.assertj.core.api.Assertions.assertThatCode; + +class DashboardStateEntityFilterValidationTest extends AbstractDashboardDataValidatorTest { + + @Test + void shouldAcceptStateEntityFilterWithoutAnyFields() { + Dashboard dashboard = filterAlias("Selected Device", """ + {"type": "stateEntity"}"""); + + assertThatCode(() -> validate(dashboard)).doesNotThrowAnyException(); + } + + @Test + void shouldAcceptStateEntityFilterWithBothFieldsSet() { + Dashboard dashboard = filterAlias("Selected Device", """ + { + "type": "stateEntity", + "stateEntityParamName": "deviceId", + "defaultStateEntity": {"entityType": "DEVICE", "id": "%s"} + }""".formatted(ALIAS_UUID)); + + assertThatCode(() -> validate(dashboard)).doesNotThrowAnyException(); + } + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardViolationAggregationTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardViolationAggregationTest.java new file mode 100644 index 0000000000..a0cbec4630 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/service/validator/dashboard/DashboardViolationAggregationTest.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2026 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.dao.service.validator.dashboard; + +import org.thingsboard.server.dao.service.validator.AbstractDashboardDataValidatorTest; + +import org.junit.jupiter.api.Test; +import org.thingsboard.server.common.data.Dashboard; +import org.thingsboard.server.dao.exception.DataValidationException; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class DashboardViolationAggregationTest extends AbstractDashboardDataValidatorTest { + + @Test + void shouldAggregateMultipleViolations() { + Dashboard dashboard = filterAlias("Devices", """ + {"type": "entityList", "entityList": []}"""); + + assertThatThrownBy(() -> validate(dashboard)) + .isInstanceOf(DataValidationException.class) + .hasMessage("Dashboard validation error: alias 'Devices' field 'entityList' must not be empty, alias 'Devices' field 'entityType' must not be null"); + } + +}