From dfe1c046f8cb0e22105fb1acdc659cef1362bd21 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Fri, 19 Jun 2026 12:46:08 +0300 Subject: [PATCH] Add real-context integration test for LTS 4.2.2.3 migration --- .../lts/LtsMigrationIntegrationTest.java | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 application/src/test/java/org/thingsboard/server/service/install/lts/LtsMigrationIntegrationTest.java diff --git a/application/src/test/java/org/thingsboard/server/service/install/lts/LtsMigrationIntegrationTest.java b/application/src/test/java/org/thingsboard/server/service/install/lts/LtsMigrationIntegrationTest.java new file mode 100644 index 0000000000..1018ec6f2b --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/install/lts/LtsMigrationIntegrationTest.java @@ -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); + } +}