21 changed files with 1175 additions and 237 deletions
@ -0,0 +1,21 @@ |
|||
-- |
|||
-- 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. |
|||
-- |
|||
|
|||
-- CALCULATED FIELD ADDITIONAL INFO ADDITION START |
|||
|
|||
ALTER TABLE calculated_field ADD COLUMN IF NOT EXISTS additional_info varchar; |
|||
|
|||
-- CALCULATED FIELD ADDITIONAL INFO ADDITION END |
|||
@ -0,0 +1,37 @@ |
|||
-- |
|||
-- 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. |
|||
-- |
|||
|
|||
-- IOT HUB INSTALLED ITEM START |
|||
|
|||
CREATE TABLE IF NOT EXISTS iot_hub_installed_item ( |
|||
id UUID NOT NULL PRIMARY KEY, |
|||
created_time BIGINT NOT NULL, |
|||
tenant_id UUID NOT NULL, |
|||
item_id UUID NOT NULL, |
|||
item_version_id UUID NOT NULL, |
|||
item_name VARCHAR NOT NULL, |
|||
item_type VARCHAR NOT NULL, |
|||
version VARCHAR NOT NULL, |
|||
descriptor JSONB NOT NULL |
|||
); |
|||
|
|||
CREATE INDEX IF NOT EXISTS idx_iot_hub_installed_item_tenant_id ON iot_hub_installed_item(tenant_id); |
|||
|
|||
CREATE INDEX IF NOT EXISTS idx_iot_hub_installed_item_item_type ON iot_hub_installed_item(tenant_id, item_type); |
|||
|
|||
CREATE INDEX IF NOT EXISTS idx_iot_hub_installed_item_item_id ON iot_hub_installed_item(tenant_id, item_id); |
|||
|
|||
-- IOT HUB INSTALLED ITEM END |
|||
@ -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.service.install.lts; |
|||
|
|||
/** |
|||
* One LTS migration, keyed by the version it ships in (e.g. "4.2.2.3"). |
|||
* The runner runs the version's data/upgrade/lts/<version>/schema_update.sql by convention |
|||
* (if present), then calls {@link #apply()} for any programmatic work beyond SQL. |
|||
* Most migrations are SQL-only and only override {@link #getVersion()}. |
|||
*/ |
|||
public interface LtsMigration { |
|||
|
|||
String getVersion(); |
|||
|
|||
default void apply() { |
|||
} |
|||
} |
|||
@ -0,0 +1,139 @@ |
|||
/** |
|||
* 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.service.install.lts; |
|||
|
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.jdbc.core.JdbcTemplate; |
|||
import org.springframework.stereotype.Component; |
|||
import org.springframework.transaction.PlatformTransactionManager; |
|||
import org.springframework.transaction.support.TransactionTemplate; |
|||
import org.thingsboard.server.queue.util.TbCoreComponent; |
|||
import org.thingsboard.server.service.install.DatabaseSchemaSettingsService; |
|||
import org.thingsboard.server.service.install.InstallScripts; |
|||
|
|||
import java.io.IOException; |
|||
import java.io.UncheckedIOException; |
|||
import java.nio.file.Files; |
|||
import java.nio.file.Path; |
|||
import java.nio.file.Paths; |
|||
import java.util.Comparator; |
|||
import java.util.HashSet; |
|||
import java.util.List; |
|||
import java.util.Set; |
|||
|
|||
@Slf4j |
|||
@Component |
|||
@TbCoreComponent |
|||
public class LtsMigrationService { |
|||
|
|||
private static final String SCHEMA_UPDATE_SQL = "schema_update.sql"; |
|||
|
|||
private final JdbcTemplate jdbcTemplate; |
|||
private final InstallScripts installScripts; |
|||
private final DatabaseSchemaSettingsService schemaSettingsService; |
|||
private final TransactionTemplate transactionTemplate; |
|||
private final List<LtsMigration> migrations; |
|||
|
|||
public LtsMigrationService(JdbcTemplate jdbcTemplate, |
|||
InstallScripts installScripts, |
|||
DatabaseSchemaSettingsService schemaSettingsService, |
|||
PlatformTransactionManager transactionManager, |
|||
List<LtsMigration> migrations) { |
|||
this.jdbcTemplate = jdbcTemplate; |
|||
this.installScripts = installScripts; |
|||
this.schemaSettingsService = schemaSettingsService; |
|||
this.transactionTemplate = new TransactionTemplate(transactionManager); |
|||
this.migrations = validateAndSort(migrations); |
|||
log.info("Discovered {} LTS migration(s): {}", this.migrations.size(), |
|||
this.migrations.stream().map(LtsMigration::getVersion).toList()); |
|||
} |
|||
|
|||
private static List<LtsMigration> validateAndSort(List<LtsMigration> migrations) { |
|||
Set<String> seen = new HashSet<>(); |
|||
for (LtsMigration m : migrations) { |
|||
LtsVersion.parse(m.getVersion()); // fail loud on unparseable version
|
|||
if (!seen.add(m.getVersion())) { |
|||
throw new IllegalStateException("Duplicate LTS migration version: " + m.getVersion()); |
|||
} |
|||
} |
|||
return migrations.stream() |
|||
.sorted(Comparator.comparing(m -> LtsVersion.parse(m.getVersion()))) |
|||
.toList(); |
|||
} |
|||
|
|||
/** No-downtime path: per migration in (from, to] run SQL, then apply(), then record the version. */ |
|||
public void applyMigrations(String fromVersion, String toVersion) { |
|||
for (LtsMigration migration : select(fromVersion, toVersion)) { |
|||
String version = migration.getVersion(); |
|||
transactionTemplate.executeWithoutResult(status -> { |
|||
runSchemaUpdate(version); |
|||
migration.apply(); |
|||
schemaSettingsService.updateSchemaVersion(version); |
|||
}); |
|||
log.info("Applied LTS migration {}", version); |
|||
} |
|||
} |
|||
|
|||
/** Offline major-upgrade schema phase: per migration in (from, to] run SQL only. No version record. */ |
|||
public void runSchemaMigrations(String fromVersion, String toVersion) { |
|||
for (LtsMigration migration : select(fromVersion, toVersion)) { |
|||
String version = migration.getVersion(); |
|||
transactionTemplate.executeWithoutResult(status -> runSchemaUpdate(version)); |
|||
log.info("Applied LTS schema migration {}", version); |
|||
} |
|||
} |
|||
|
|||
/** Offline major-upgrade data phase: per migration in (from, to] run apply() only. No SQL, no version record. */ |
|||
public void runDataMigrations(String fromVersion, String toVersion) { |
|||
for (LtsMigration migration : select(fromVersion, toVersion)) { |
|||
migration.apply(); |
|||
log.info("Applied LTS data migration {}", migration.getVersion()); |
|||
} |
|||
} |
|||
|
|||
private List<LtsMigration> select(String fromVersion, String toVersion) { |
|||
LtsVersion from = LtsVersion.parse(fromVersion); |
|||
LtsVersion to = LtsVersion.parse(toVersion); |
|||
return migrations.stream() |
|||
.filter(migration -> { |
|||
LtsVersion version = LtsVersion.parse(migration.getVersion()); |
|||
// Run only migrations whose family matches the target version. Older-family
|
|||
// migrations (e.g. 4.2.x) ride onto newer-family branches (e.g. 4.3.x) via the
|
|||
// release-merge cascade, but each branch's own family of migrations is
|
|||
// self-contained (newer-family copies reproduce the older schema/data changes),
|
|||
// so a cross-family upgrade is fully handled by the target-family migrations.
|
|||
// Excluding the dormant older-family beans avoids double-processing.
|
|||
return version.sameFamily(to) |
|||
&& version.compareTo(from) > 0 |
|||
&& version.compareTo(to) <= 0; |
|||
}) |
|||
.toList(); |
|||
} |
|||
|
|||
private void runSchemaUpdate(String version) { |
|||
Path sqlFile = Paths.get(installScripts.getDataDir(), "upgrade", "lts", version, SCHEMA_UPDATE_SQL); |
|||
if (!Files.exists(sqlFile)) { |
|||
log.trace("No LTS schema update file for version {} at {}", version, sqlFile); |
|||
return; |
|||
} |
|||
try { |
|||
jdbcTemplate.execute(Files.readString(sqlFile)); |
|||
log.info("Applied LTS SQL schema update from {}", sqlFile); |
|||
} catch (IOException e) { |
|||
throw new UncheckedIOException("Failed to read LTS schema update file: " + sqlFile, e); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,61 @@ |
|||
/** |
|||
* 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.service.install.lts; |
|||
|
|||
public record LtsVersion(int major, int minor, int maintenance, int patch) implements Comparable<LtsVersion> { |
|||
|
|||
public static LtsVersion parse(String version) { |
|||
if (version == null) { |
|||
throw new IllegalArgumentException("Version is null"); |
|||
} |
|||
String[] parts = version.split("\\."); |
|||
try { |
|||
int major = Integer.parseInt(parts[0]); |
|||
int minor = parts.length > 1 ? Integer.parseInt(parts[1]) : 0; |
|||
int maintenance = parts.length > 2 ? Integer.parseInt(parts[2]) : 0; |
|||
int patch = parts.length > 3 ? Integer.parseInt(parts[3]) : 0; |
|||
return new LtsVersion(major, minor, maintenance, patch); |
|||
} catch (NumberFormatException e) { |
|||
throw new IllegalArgumentException("Invalid version: " + version, e); |
|||
} |
|||
} |
|||
|
|||
public boolean sameFamily(LtsVersion other) { |
|||
return major == other.major && minor == other.minor; |
|||
} |
|||
|
|||
@Override |
|||
public int compareTo(LtsVersion o) { |
|||
int c = Integer.compare(major, o.major); |
|||
if (c != 0) { |
|||
return c; |
|||
} |
|||
c = Integer.compare(minor, o.minor); |
|||
if (c != 0) { |
|||
return c; |
|||
} |
|||
c = Integer.compare(maintenance, o.maintenance); |
|||
if (c != 0) { |
|||
return c; |
|||
} |
|||
return Integer.compare(patch, o.patch); |
|||
} |
|||
|
|||
@Override |
|||
public String toString() { |
|||
return major + "." + minor + "." + maintenance + "." + patch; |
|||
} |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
/** |
|||
* 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.service.install.lts; |
|||
|
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.stereotype.Component; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.widget.WidgetTypeDetails; |
|||
import org.thingsboard.server.common.data.widget.WidgetsBundle; |
|||
import org.thingsboard.server.dao.widget.WidgetTypeService; |
|||
import org.thingsboard.server.dao.widget.WidgetsBundleService; |
|||
import org.thingsboard.server.queue.util.TbCoreComponent; |
|||
|
|||
import java.util.List; |
|||
|
|||
@Slf4j |
|||
@Component |
|||
@TbCoreComponent |
|||
@RequiredArgsConstructor |
|||
public class V4_2_2_3Migration implements LtsMigration { |
|||
|
|||
// Match on the bundle ALIAS, not the JSON filename stem.
|
|||
// The "industrial_widgets" bundle shipped in a misspelled file (industial_widgets.json),
|
|||
// but the alias inside it is correctly spelled.
|
|||
private static final List<String> OBSOLETE_BUNDLE_ALIASES = List.of( |
|||
"air_quality", "indoor_environment", "industrial_widgets", "outdoor_environment"); |
|||
|
|||
private final WidgetsBundleService widgetsBundleService; |
|||
private final WidgetTypeService widgetTypeService; |
|||
|
|||
@Override |
|||
public String getVersion() { |
|||
return "4.2.2.3"; |
|||
} |
|||
|
|||
@Override |
|||
public void apply() { |
|||
for (String alias : OBSOLETE_BUNDLE_ALIASES) { |
|||
deprecateTypesAndDeleteBundle(alias); |
|||
} |
|||
} |
|||
|
|||
// Marks the bundle's widget types as deprecated (keeping the type entities) and deletes ONLY the bundle entity.
|
|||
private void deprecateTypesAndDeleteBundle(String alias) { |
|||
WidgetsBundle bundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, alias); |
|||
if (bundle == null) { |
|||
return; // already removed — idempotent
|
|||
} |
|||
List<WidgetTypeDetails> types = widgetTypeService.findWidgetTypesDetailsByWidgetsBundleId(TenantId.SYS_TENANT_ID, bundle.getId()); |
|||
for (WidgetTypeDetails type : types) { |
|||
if (!type.isDeprecated()) { |
|||
type.setDeprecated(true); |
|||
widgetTypeService.saveWidgetType(type); |
|||
} |
|||
} |
|||
widgetsBundleService.deleteWidgetsBundle(TenantId.SYS_TENANT_ID, bundle.getId()); |
|||
log.info("Deprecated {} widget type(s) and removed obsolete system widget bundle: {}", types.size(), alias); |
|||
} |
|||
} |
|||
@ -0,0 +1,29 @@ |
|||
/** |
|||
* 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.service.install.lts; |
|||
|
|||
import org.springframework.stereotype.Component; |
|||
import org.thingsboard.server.queue.util.TbCoreComponent; |
|||
|
|||
@Component |
|||
@TbCoreComponent |
|||
public class V4_3_1_2Migration implements LtsMigration { |
|||
|
|||
@Override |
|||
public String getVersion() { |
|||
return "4.3.1.2"; |
|||
} |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
/** |
|||
* 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.service.install.lts; |
|||
|
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.stereotype.Component; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.widget.WidgetTypeDetails; |
|||
import org.thingsboard.server.common.data.widget.WidgetsBundle; |
|||
import org.thingsboard.server.dao.widget.WidgetTypeService; |
|||
import org.thingsboard.server.dao.widget.WidgetsBundleService; |
|||
import org.thingsboard.server.queue.util.TbCoreComponent; |
|||
|
|||
import java.util.List; |
|||
|
|||
@Slf4j |
|||
@Component |
|||
@TbCoreComponent |
|||
@RequiredArgsConstructor |
|||
public class V4_3_1_3Migration implements LtsMigration { |
|||
|
|||
// Match on the bundle ALIAS, not the JSON filename stem.
|
|||
// The "industrial_widgets" bundle shipped in a misspelled file (industial_widgets.json),
|
|||
// but the alias inside it is correctly spelled.
|
|||
private static final List<String> OBSOLETE_BUNDLE_ALIASES = List.of( |
|||
"air_quality", "indoor_environment", "industrial_widgets", "outdoor_environment"); |
|||
|
|||
private final WidgetsBundleService widgetsBundleService; |
|||
private final WidgetTypeService widgetTypeService; |
|||
|
|||
@Override |
|||
public String getVersion() { |
|||
return "4.3.1.3"; |
|||
} |
|||
|
|||
@Override |
|||
public void apply() { |
|||
for (String alias : OBSOLETE_BUNDLE_ALIASES) { |
|||
deprecateTypesAndDeleteBundle(alias); |
|||
} |
|||
} |
|||
|
|||
// Marks the bundle's widget types as deprecated (keeping the type entities) and deletes ONLY the bundle entity.
|
|||
private void deprecateTypesAndDeleteBundle(String alias) { |
|||
WidgetsBundle bundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, alias); |
|||
if (bundle == null) { |
|||
return; // already removed — idempotent
|
|||
} |
|||
List<WidgetTypeDetails> types = widgetTypeService.findWidgetTypesDetailsByWidgetsBundleId(TenantId.SYS_TENANT_ID, bundle.getId()); |
|||
for (WidgetTypeDetails type : types) { |
|||
if (!type.isDeprecated()) { |
|||
type.setDeprecated(true); |
|||
widgetTypeService.saveWidgetType(type); |
|||
} |
|||
} |
|||
widgetsBundleService.deleteWidgetsBundle(TenantId.SYS_TENANT_ID, bundle.getId()); |
|||
log.info("Deprecated {} widget type(s) and removed obsolete system widget bundle: {}", types.size(), alias); |
|||
} |
|||
} |
|||
@ -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.service.install; |
|||
|
|||
import org.junit.jupiter.api.Test; |
|||
import org.junit.jupiter.api.extension.ExtendWith; |
|||
import org.mockito.InjectMocks; |
|||
import org.mockito.Mock; |
|||
import org.mockito.junit.jupiter.MockitoExtension; |
|||
import org.springframework.jdbc.core.JdbcTemplate; |
|||
|
|||
import static org.mockito.Mockito.verify; |
|||
|
|||
@ExtendWith(MockitoExtension.class) |
|||
class DefaultDatabaseSchemaSettingsServiceTest { |
|||
|
|||
@Mock |
|||
private ProjectInfo projectInfo; |
|||
|
|||
@Mock |
|||
private JdbcTemplate jdbcTemplate; |
|||
|
|||
@InjectMocks |
|||
private DefaultDatabaseSchemaSettingsService service; |
|||
|
|||
@Test |
|||
void updateSchemaVersionWithExplicitVersionEncodesAsLong() { |
|||
service.updateSchemaVersion("4.2.2.3"); |
|||
verify(jdbcTemplate).execute("UPDATE tb_schema_settings SET schema_version = 4002002003"); |
|||
} |
|||
|
|||
@Test |
|||
void updateSchemaVersionWithShortVersionPadsMissingComponents() { |
|||
service.updateSchemaVersion("4.3"); |
|||
verify(jdbcTemplate).execute("UPDATE tb_schema_settings SET schema_version = 4003000000"); |
|||
} |
|||
} |
|||
@ -0,0 +1,144 @@ |
|||
/** |
|||
* 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.service.install.lts; |
|||
|
|||
import com.fasterxml.jackson.databind.JsonNode; |
|||
import org.junit.After; |
|||
import org.junit.Before; |
|||
import org.junit.Test; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.jdbc.core.JdbcTemplate; |
|||
import org.thingsboard.common.util.JacksonUtil; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.id.WidgetTypeId; |
|||
import org.thingsboard.server.common.data.id.WidgetsBundleId; |
|||
import org.thingsboard.server.common.data.widget.WidgetTypeDetails; |
|||
import org.thingsboard.server.common.data.widget.WidgetsBundle; |
|||
import org.thingsboard.server.controller.AbstractControllerTest; |
|||
import org.thingsboard.server.dao.service.DaoSqlTest; |
|||
import org.thingsboard.server.dao.widget.WidgetTypeService; |
|||
import org.thingsboard.server.dao.widget.WidgetsBundleService; |
|||
|
|||
import java.util.List; |
|||
import java.util.UUID; |
|||
|
|||
import static org.junit.Assert.assertEquals; |
|||
import static org.junit.Assert.assertNotNull; |
|||
import static org.junit.Assert.assertNull; |
|||
import static org.junit.Assert.assertTrue; |
|||
|
|||
@DaoSqlTest |
|||
public class LtsMigrationIntegrationTest extends AbstractControllerTest { |
|||
|
|||
private static final long V_4_2_2_2 = 4_002_002_002L; |
|||
private static final long V_4_2_2_3 = 4_002_002_003L; |
|||
private static final String OBSOLETE_ALIAS = "air_quality"; |
|||
|
|||
@Autowired |
|||
private LtsMigrationService ltsMigrationService; |
|||
@Autowired |
|||
private WidgetsBundleService widgetsBundleService; |
|||
@Autowired |
|||
private WidgetTypeService widgetTypeService; |
|||
@Autowired |
|||
private JdbcTemplate jdbcTemplate; |
|||
|
|||
private Long originalSchemaVersion; |
|||
private WidgetsBundleId bundleId; |
|||
private WidgetTypeId widgetTypeId; |
|||
|
|||
@Before |
|||
public void setUp() { |
|||
// The test-context DB (DaoSqlTest) creates tb_schema_settings empty, so ensure the baseline
|
|||
// version row a real installed DB always has exists before driving the migration.
|
|||
originalSchemaVersion = jdbcTemplate.query("SELECT schema_version FROM tb_schema_settings", |
|||
rs -> rs.next() ? rs.getLong(1) : null); |
|||
if (originalSchemaVersion == null) { |
|||
jdbcTemplate.execute("INSERT INTO tb_schema_settings (schema_version, product) VALUES (" + V_4_2_2_2 + ", 'CE')"); |
|||
} |
|||
|
|||
// Seed an obsolete system widget bundle with one (non-deprecated) widget type linked to it.
|
|||
WidgetsBundle bundle = new WidgetsBundle(); |
|||
bundle.setTenantId(TenantId.SYS_TENANT_ID); |
|||
bundle.setAlias(OBSOLETE_ALIAS); |
|||
bundle.setTitle("Air quality"); |
|||
bundleId = widgetsBundleService.saveWidgetsBundle(bundle).getId(); |
|||
|
|||
WidgetTypeDetails type = new WidgetTypeDetails(); |
|||
type.setTenantId(TenantId.SYS_TENANT_ID); |
|||
type.setFqn("air_quality_sample_" + UUID.randomUUID()); |
|||
type.setName("Air quality sample"); |
|||
type.setDescriptor(JacksonUtil.fromString("{ \"type\": \"latest\" }", JsonNode.class)); |
|||
WidgetTypeDetails saved = widgetTypeService.saveWidgetType(type); |
|||
widgetTypeId = saved.getId(); |
|||
widgetTypeService.updateWidgetsBundleWidgetFqns(TenantId.SYS_TENANT_ID, bundleId, List.of(saved.getFqn())); |
|||
|
|||
// Pretend the DB is at 4.2.2.2 so the 4.2.2.3 migration is in range (4.2.2.2, 4.2.2.3].
|
|||
jdbcTemplate.execute("UPDATE tb_schema_settings SET schema_version = " + V_4_2_2_2); |
|||
} |
|||
|
|||
@After |
|||
public void tearDown() { |
|||
// Restore the schema version (or drop the row we seeded) and clean up whatever the migration left behind.
|
|||
if (originalSchemaVersion != null) { |
|||
jdbcTemplate.execute("UPDATE tb_schema_settings SET schema_version = " + originalSchemaVersion); |
|||
} else { |
|||
jdbcTemplate.execute("DELETE FROM tb_schema_settings"); |
|||
} |
|||
if (widgetTypeId != null && widgetTypeService.findWidgetTypeDetailsById(TenantId.SYS_TENANT_ID, widgetTypeId) != null) { |
|||
widgetTypeService.deleteWidgetType(TenantId.SYS_TENANT_ID, widgetTypeId); |
|||
} |
|||
WidgetsBundle bundle = widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, OBSOLETE_ALIAS); |
|||
if (bundle != null) { |
|||
widgetsBundleService.deleteWidgetsBundle(TenantId.SYS_TENANT_ID, bundle.getId()); |
|||
} |
|||
} |
|||
|
|||
@Test |
|||
public void appliesSqlDeprecatesTypesDeletesBundleAndRecordsVersion() { |
|||
ltsMigrationService.applyMigrations("4.2.2.2", "4.2.2.3"); |
|||
|
|||
// 1. The version's SQL ran: iot_hub_installed_item table now exists.
|
|||
assertTrue(tableExists("iot_hub_installed_item")); |
|||
// 2. The version was recorded.
|
|||
assertEquals(Long.valueOf(V_4_2_2_3), |
|||
jdbcTemplate.queryForObject("SELECT schema_version FROM tb_schema_settings", Long.class)); |
|||
// 3. The obsolete bundle entity was deleted.
|
|||
assertNull(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, OBSOLETE_ALIAS)); |
|||
// 4. The widget type was KEPT and marked deprecated.
|
|||
WidgetTypeDetails type = widgetTypeService.findWidgetTypeDetailsById(TenantId.SYS_TENANT_ID, widgetTypeId); |
|||
assertNotNull(type); |
|||
assertTrue(type.isDeprecated()); |
|||
} |
|||
|
|||
@Test |
|||
public void reRunFromCurrentVersionIsNoOp() { |
|||
ltsMigrationService.applyMigrations("4.2.2.2", "4.2.2.3"); |
|||
// Re-running from the now-current version selects an empty range — nothing changes, nothing throws.
|
|||
ltsMigrationService.applyMigrations("4.2.2.3", "4.2.2.3"); |
|||
|
|||
assertEquals(Long.valueOf(V_4_2_2_3), |
|||
jdbcTemplate.queryForObject("SELECT schema_version FROM tb_schema_settings", Long.class)); |
|||
assertNull(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, OBSOLETE_ALIAS)); |
|||
assertTrue(widgetTypeService.findWidgetTypeDetailsById(TenantId.SYS_TENANT_ID, widgetTypeId).isDeprecated()); |
|||
} |
|||
|
|||
private boolean tableExists(String table) { |
|||
Boolean exists = jdbcTemplate.queryForObject( |
|||
"SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = ?)", Boolean.class, table); |
|||
return Boolean.TRUE.equals(exists); |
|||
} |
|||
} |
|||
@ -0,0 +1,194 @@ |
|||
/** |
|||
* 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.service.install.lts; |
|||
|
|||
import org.junit.jupiter.api.BeforeEach; |
|||
import org.junit.jupiter.api.Test; |
|||
import org.junit.jupiter.api.io.TempDir; |
|||
import org.mockito.Mockito; |
|||
import org.springframework.jdbc.core.JdbcTemplate; |
|||
import org.springframework.transaction.PlatformTransactionManager; |
|||
import org.springframework.transaction.support.SimpleTransactionStatus; |
|||
import org.thingsboard.server.service.install.DatabaseSchemaSettingsService; |
|||
import org.thingsboard.server.service.install.InstallScripts; |
|||
|
|||
import java.nio.file.Files; |
|||
import java.nio.file.Path; |
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
import static org.junit.jupiter.api.Assertions.assertEquals; |
|||
import static org.junit.jupiter.api.Assertions.assertThrows; |
|||
import static org.mockito.ArgumentMatchers.any; |
|||
import static org.mockito.ArgumentMatchers.anyString; |
|||
import static org.mockito.Mockito.never; |
|||
import static org.mockito.Mockito.verify; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
class LtsMigrationServiceTest { |
|||
|
|||
private JdbcTemplate jdbcTemplate; |
|||
private InstallScripts installScripts; |
|||
private DatabaseSchemaSettingsService schemaSettingsService; |
|||
private PlatformTransactionManager txManager; |
|||
|
|||
@TempDir |
|||
Path dataDir; |
|||
|
|||
@BeforeEach |
|||
void setUp() { |
|||
jdbcTemplate = Mockito.mock(JdbcTemplate.class); |
|||
installScripts = Mockito.mock(InstallScripts.class); |
|||
schemaSettingsService = Mockito.mock(DatabaseSchemaSettingsService.class); |
|||
txManager = Mockito.mock(PlatformTransactionManager.class); |
|||
when(txManager.getTransaction(any())).thenReturn(new SimpleTransactionStatus()); |
|||
when(installScripts.getDataDir()).thenReturn(dataDir.toString()); |
|||
} |
|||
|
|||
private void writeSql(String version, String sql) throws Exception { |
|||
Path dir = dataDir.resolve("upgrade").resolve("lts").resolve(version); |
|||
Files.createDirectories(dir); |
|||
Files.writeString(dir.resolve("schema_update.sql"), sql); |
|||
} |
|||
|
|||
/** Records which apply() hooks fired, in order. */ |
|||
private LtsMigration migration(String version, List<String> applied) { |
|||
return new LtsMigration() { |
|||
@Override public String getVersion() { return version; } |
|||
@Override public void apply() { applied.add(version); } |
|||
}; |
|||
} |
|||
|
|||
private LtsMigrationService service(List<LtsMigration> migrations) { |
|||
return new LtsMigrationService(jdbcTemplate, installScripts, schemaSettingsService, txManager, migrations); |
|||
} |
|||
|
|||
@Test |
|||
void selectsOnlyInRangeMigrationsInAscendingOrder() throws Exception { |
|||
List<String> applied = new ArrayList<>(); |
|||
writeSql("4.2.2.3", "SELECT 1;"); |
|||
writeSql("4.2.2.4", "SELECT 2;"); |
|||
// intentionally unsorted input; service must sort ascending
|
|||
LtsMigrationService service = service(List.of( |
|||
migration("4.2.2.4", applied), |
|||
migration("4.2.2.2", applied), |
|||
migration("4.2.2.3", applied))); |
|||
|
|||
service.applyMigrations("4.2.2.2", "4.2.2.3"); |
|||
|
|||
// only 4.2.2.3 is in (4.2.2.2, 4.2.2.3]
|
|||
assertEquals(List.of("4.2.2.3"), applied); |
|||
verify(jdbcTemplate).execute("SELECT 1;"); |
|||
verify(jdbcTemplate, never()).execute("SELECT 2;"); |
|||
verify(schemaSettingsService).updateSchemaVersion("4.2.2.3"); |
|||
} |
|||
|
|||
@Test |
|||
void appliesAllInRangeAndRecordsEachVersion() throws Exception { |
|||
List<String> applied = new ArrayList<>(); |
|||
writeSql("4.2.2.3", "SELECT 1;"); |
|||
writeSql("4.2.2.4", "SELECT 2;"); |
|||
LtsMigrationService service = service(List.of( |
|||
migration("4.2.2.3", applied), migration("4.2.2.4", applied))); |
|||
|
|||
service.applyMigrations("4.2.2.2", "4.2.2.4"); |
|||
|
|||
assertEquals(List.of("4.2.2.3", "4.2.2.4"), applied); |
|||
verify(jdbcTemplate).execute("SELECT 1;"); |
|||
verify(jdbcTemplate).execute("SELECT 2;"); |
|||
verify(schemaSettingsService).updateSchemaVersion("4.2.2.3"); |
|||
verify(schemaSettingsService).updateSchemaVersion("4.2.2.4"); |
|||
} |
|||
|
|||
@Test |
|||
void skipsMigrationsOutsideTargetFamilyOnCrossFamilyUpgrade() { |
|||
List<String> applied = new ArrayList<>(); |
|||
// 4.2.2.3 is carried onto a 4.3 branch by the release-merge cascade but must stay dormant:
|
|||
// a cross-family 4.2 -> 4.3 upgrade is handled entirely by the 4.3-family migrations.
|
|||
LtsMigrationService service = service(List.of( |
|||
migration("4.2.2.3", applied), |
|||
migration("4.3.1.2", applied), |
|||
migration("4.3.1.3", applied))); |
|||
|
|||
service.runDataMigrations("4.2.2.2", "4.3.1.3"); |
|||
|
|||
assertEquals(List.of("4.3.1.2", "4.3.1.3"), applied); |
|||
} |
|||
|
|||
@Test |
|||
void reRunAtCurrentVersionIsNoOp() throws Exception { |
|||
List<String> applied = new ArrayList<>(); |
|||
writeSql("4.2.2.3", "SELECT 1;"); |
|||
LtsMigrationService service = service(List.of(migration("4.2.2.3", applied))); |
|||
|
|||
service.applyMigrations("4.2.2.3", "4.2.2.3"); |
|||
|
|||
assertEquals(List.of(), applied); |
|||
verify(jdbcTemplate, never()).execute(anyString()); |
|||
verify(schemaSettingsService, never()).updateSchemaVersion(anyString()); |
|||
} |
|||
|
|||
@Test |
|||
void runSchemaMigrationsRunsSqlButNeverAppliesOrRecords() throws Exception { |
|||
List<String> applied = new ArrayList<>(); |
|||
writeSql("4.2.2.3", "SELECT 1;"); |
|||
LtsMigrationService service = service(List.of(migration("4.2.2.3", applied))); |
|||
|
|||
service.runSchemaMigrations("4.2.2.2", "4.2.2.3"); |
|||
|
|||
verify(jdbcTemplate).execute("SELECT 1;"); |
|||
assertEquals(List.of(), applied); |
|||
verify(schemaSettingsService, never()).updateSchemaVersion(anyString()); |
|||
} |
|||
|
|||
@Test |
|||
void runDataMigrationsAppliesButNeverRunsSqlOrRecords() throws Exception { |
|||
List<String> applied = new ArrayList<>(); |
|||
writeSql("4.2.2.3", "SELECT 1;"); |
|||
LtsMigrationService service = service(List.of(migration("4.2.2.3", applied))); |
|||
|
|||
service.runDataMigrations("4.2.2.2", "4.2.2.3"); |
|||
|
|||
assertEquals(List.of("4.2.2.3"), applied); |
|||
verify(jdbcTemplate, never()).execute(anyString()); |
|||
verify(schemaSettingsService, never()).updateSchemaVersion(anyString()); |
|||
} |
|||
|
|||
@Test |
|||
void migrationWithoutSqlFileStillAppliesAndRecords() { |
|||
List<String> applied = new ArrayList<>(); |
|||
LtsMigrationService service = service(List.of(migration("4.2.2.3", applied))); |
|||
|
|||
service.applyMigrations("4.2.2.2", "4.2.2.3"); |
|||
|
|||
assertEquals(List.of("4.2.2.3"), applied); |
|||
verify(jdbcTemplate, never()).execute(anyString()); |
|||
verify(schemaSettingsService).updateSchemaVersion("4.2.2.3"); |
|||
} |
|||
|
|||
@Test |
|||
void failsLoudOnDuplicateVersion() { |
|||
List<String> applied = new ArrayList<>(); |
|||
assertThrows(IllegalStateException.class, () -> service(List.of( |
|||
migration("4.2.2.3", applied), migration("4.2.2.3", applied)))); |
|||
} |
|||
|
|||
@Test |
|||
void failsLoudOnUnparseableVersion() { |
|||
List<String> applied = new ArrayList<>(); |
|||
assertThrows(IllegalArgumentException.class, () -> service(List.of(migration("nope", applied)))); |
|||
} |
|||
} |
|||
@ -0,0 +1,78 @@ |
|||
/** |
|||
* 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.service.install.lts; |
|||
|
|||
import org.junit.jupiter.api.Test; |
|||
import org.junit.jupiter.params.ParameterizedTest; |
|||
import org.junit.jupiter.params.provider.CsvSource; |
|||
import org.junit.jupiter.params.provider.ValueSource; |
|||
|
|||
import static org.junit.jupiter.api.Assertions.assertEquals; |
|||
import static org.junit.jupiter.api.Assertions.assertFalse; |
|||
import static org.junit.jupiter.api.Assertions.assertThrows; |
|||
import static org.junit.jupiter.api.Assertions.assertTrue; |
|||
|
|||
class LtsVersionTest { |
|||
|
|||
@ParameterizedTest |
|||
@CsvSource({ |
|||
"4.2.1, 4, 2, 1, 0", |
|||
"4.2.0, 4, 2, 0, 0", |
|||
"4.2, 4, 2, 0, 0", |
|||
"4.0.1.2, 4, 0, 1, 2", |
|||
"4, 4, 0, 0, 0", |
|||
"10.20.30.40, 10, 20, 30, 40" |
|||
}) |
|||
void parsesValidVersions(String input, int major, int minor, int maintenance, int patch) { |
|||
LtsVersion v = LtsVersion.parse(input); |
|||
assertEquals(major, v.major()); |
|||
assertEquals(minor, v.minor()); |
|||
assertEquals(maintenance, v.maintenance()); |
|||
assertEquals(patch, v.patch()); |
|||
} |
|||
|
|||
@ParameterizedTest |
|||
@ValueSource(strings = {"invalid", "a.b.c", "1.2.y.x", "1.x.3", ""}) |
|||
void throwsOnInvalidVersions(String input) { |
|||
assertThrows(IllegalArgumentException.class, () -> LtsVersion.parse(input)); |
|||
} |
|||
|
|||
@Test |
|||
void throwsOnNull() { |
|||
assertThrows(IllegalArgumentException.class, () -> LtsVersion.parse(null)); |
|||
} |
|||
|
|||
@Test |
|||
void comparesByEachComponent() { |
|||
assertTrue(LtsVersion.parse("4.2.2.3").compareTo(LtsVersion.parse("4.2.2.2")) > 0); |
|||
assertTrue(LtsVersion.parse("4.2.2.0").compareTo(LtsVersion.parse("4.2.1.9")) > 0); |
|||
assertTrue(LtsVersion.parse("4.3.0.0").compareTo(LtsVersion.parse("4.2.9.9")) > 0); |
|||
assertEquals(0, LtsVersion.parse("4.2.2.3").compareTo(LtsVersion.parse("4.2.2.3"))); |
|||
} |
|||
|
|||
@Test |
|||
void sameFamilyComparesMajorAndMinorOnly() { |
|||
assertTrue(LtsVersion.parse("4.2.2.3").sameFamily(LtsVersion.parse("4.2.0.0"))); |
|||
assertFalse(LtsVersion.parse("4.3.0.0").sameFamily(LtsVersion.parse("4.2.9.9"))); |
|||
assertFalse(LtsVersion.parse("5.2.0.0").sameFamily(LtsVersion.parse("4.2.0.0"))); |
|||
} |
|||
|
|||
@Test |
|||
void toStringIsFourComponentDotForm() { |
|||
assertEquals("4.2.2.3", LtsVersion.parse("4.2.2.3").toString()); |
|||
assertEquals("4.2.0.0", LtsVersion.parse("4.2").toString()); |
|||
} |
|||
} |
|||
@ -0,0 +1,96 @@ |
|||
/** |
|||
* 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.service.install.lts; |
|||
|
|||
import org.junit.jupiter.api.Test; |
|||
import org.junit.jupiter.api.extension.ExtendWith; |
|||
import org.mockito.InjectMocks; |
|||
import org.mockito.Mock; |
|||
import org.mockito.junit.jupiter.MockitoExtension; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.id.WidgetTypeId; |
|||
import org.thingsboard.server.common.data.id.WidgetsBundleId; |
|||
import org.thingsboard.server.common.data.widget.WidgetTypeDetails; |
|||
import org.thingsboard.server.common.data.widget.WidgetsBundle; |
|||
import org.thingsboard.server.dao.widget.WidgetTypeService; |
|||
import org.thingsboard.server.dao.widget.WidgetsBundleService; |
|||
|
|||
import java.util.List; |
|||
import java.util.UUID; |
|||
|
|||
import static org.junit.jupiter.api.Assertions.assertEquals; |
|||
import static org.mockito.ArgumentMatchers.any; |
|||
import static org.mockito.ArgumentMatchers.argThat; |
|||
import static org.mockito.Mockito.never; |
|||
import static org.mockito.Mockito.verify; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
@ExtendWith(MockitoExtension.class) |
|||
class V4_2_2_3MigrationTest { |
|||
|
|||
@Mock |
|||
private WidgetsBundleService widgetsBundleService; |
|||
|
|||
@Mock |
|||
private WidgetTypeService widgetTypeService; |
|||
|
|||
@InjectMocks |
|||
private V4_2_2_3Migration migration; |
|||
|
|||
@Test |
|||
void versionIs4223() { |
|||
assertEquals("4.2.2.3", migration.getVersion()); |
|||
} |
|||
|
|||
@Test |
|||
void deprecatesTypesThenDeletesBundleEntityOnly() { |
|||
WidgetsBundle bundle = new WidgetsBundle(); |
|||
bundle.setId(new WidgetsBundleId(UUID.randomUUID())); |
|||
bundle.setAlias("air_quality"); |
|||
|
|||
WidgetTypeDetails fresh = new WidgetTypeDetails(); |
|||
fresh.setId(new WidgetTypeId(UUID.randomUUID())); |
|||
fresh.setDeprecated(false); |
|||
WidgetTypeDetails already = new WidgetTypeDetails(); |
|||
already.setId(new WidgetTypeId(UUID.randomUUID())); |
|||
already.setDeprecated(true); |
|||
|
|||
when(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, "air_quality")).thenReturn(bundle); |
|||
when(widgetTypeService.findWidgetTypesDetailsByWidgetsBundleId(TenantId.SYS_TENANT_ID, bundle.getId())).thenReturn(List.of(fresh, already)); |
|||
when(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, "indoor_environment")).thenReturn(null); |
|||
when(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, "industrial_widgets")).thenReturn(null); |
|||
when(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, "outdoor_environment")).thenReturn(null); |
|||
|
|||
migration.apply(); |
|||
|
|||
// only the previously non-deprecated type is re-saved, now deprecated
|
|||
verify(widgetTypeService).saveWidgetType(argThat(WidgetTypeDetails::isDeprecated)); |
|||
// widget types are NOT deleted
|
|||
verify(widgetTypeService, never()).deleteWidgetTypesByBundleId(any(), any()); |
|||
// only the bundle entity is removed
|
|||
verify(widgetsBundleService).deleteWidgetsBundle(TenantId.SYS_TENANT_ID, bundle.getId()); |
|||
} |
|||
|
|||
@Test |
|||
void absentBundleIsNoOp() { |
|||
when(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(any(), any())).thenReturn(null); |
|||
|
|||
migration.apply(); |
|||
|
|||
verify(widgetTypeService, never()).saveWidgetType(any()); |
|||
verify(widgetsBundleService, never()).deleteWidgetsBundle(any(), any()); |
|||
} |
|||
} |
|||
@ -0,0 +1,96 @@ |
|||
/** |
|||
* 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.service.install.lts; |
|||
|
|||
import org.junit.jupiter.api.Test; |
|||
import org.junit.jupiter.api.extension.ExtendWith; |
|||
import org.mockito.InjectMocks; |
|||
import org.mockito.Mock; |
|||
import org.mockito.junit.jupiter.MockitoExtension; |
|||
import org.thingsboard.server.common.data.id.TenantId; |
|||
import org.thingsboard.server.common.data.id.WidgetTypeId; |
|||
import org.thingsboard.server.common.data.id.WidgetsBundleId; |
|||
import org.thingsboard.server.common.data.widget.WidgetTypeDetails; |
|||
import org.thingsboard.server.common.data.widget.WidgetsBundle; |
|||
import org.thingsboard.server.dao.widget.WidgetTypeService; |
|||
import org.thingsboard.server.dao.widget.WidgetsBundleService; |
|||
|
|||
import java.util.List; |
|||
import java.util.UUID; |
|||
|
|||
import static org.junit.jupiter.api.Assertions.assertEquals; |
|||
import static org.mockito.ArgumentMatchers.any; |
|||
import static org.mockito.ArgumentMatchers.argThat; |
|||
import static org.mockito.Mockito.never; |
|||
import static org.mockito.Mockito.verify; |
|||
import static org.mockito.Mockito.when; |
|||
|
|||
@ExtendWith(MockitoExtension.class) |
|||
class V4_3_1_3MigrationTest { |
|||
|
|||
@Mock |
|||
private WidgetsBundleService widgetsBundleService; |
|||
|
|||
@Mock |
|||
private WidgetTypeService widgetTypeService; |
|||
|
|||
@InjectMocks |
|||
private V4_3_1_3Migration migration; |
|||
|
|||
@Test |
|||
void versionIs4313() { |
|||
assertEquals("4.3.1.3", migration.getVersion()); |
|||
} |
|||
|
|||
@Test |
|||
void deprecatesTypesThenDeletesBundleEntityOnly() { |
|||
WidgetsBundle bundle = new WidgetsBundle(); |
|||
bundle.setId(new WidgetsBundleId(UUID.randomUUID())); |
|||
bundle.setAlias("air_quality"); |
|||
|
|||
WidgetTypeDetails fresh = new WidgetTypeDetails(); |
|||
fresh.setId(new WidgetTypeId(UUID.randomUUID())); |
|||
fresh.setDeprecated(false); |
|||
WidgetTypeDetails already = new WidgetTypeDetails(); |
|||
already.setId(new WidgetTypeId(UUID.randomUUID())); |
|||
already.setDeprecated(true); |
|||
|
|||
when(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, "air_quality")).thenReturn(bundle); |
|||
when(widgetTypeService.findWidgetTypesDetailsByWidgetsBundleId(TenantId.SYS_TENANT_ID, bundle.getId())).thenReturn(List.of(fresh, already)); |
|||
when(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, "indoor_environment")).thenReturn(null); |
|||
when(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, "industrial_widgets")).thenReturn(null); |
|||
when(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, "outdoor_environment")).thenReturn(null); |
|||
|
|||
migration.apply(); |
|||
|
|||
// only the previously non-deprecated type is re-saved, now deprecated
|
|||
verify(widgetTypeService).saveWidgetType(argThat(WidgetTypeDetails::isDeprecated)); |
|||
// widget types are NOT deleted
|
|||
verify(widgetTypeService, never()).deleteWidgetTypesByBundleId(any(), any()); |
|||
// only the bundle entity is removed
|
|||
verify(widgetsBundleService).deleteWidgetsBundle(TenantId.SYS_TENANT_ID, bundle.getId()); |
|||
} |
|||
|
|||
@Test |
|||
void absentBundleIsNoOp() { |
|||
when(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(any(), any())).thenReturn(null); |
|||
|
|||
migration.apply(); |
|||
|
|||
verify(widgetTypeService, never()).saveWidgetType(any()); |
|||
verify(widgetsBundleService, never()).deleteWidgetsBundle(any(), any()); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue