@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.WidgetTypeId ;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails ;
import org.thingsboard.server.dao.widget.WidgetTypeService ;
import org.thingsboard.server.service.install.DatabaseSchemaSettingsService ;
import org.thingsboard.server.service.install.InstallScripts ;
import org.thingsboard.server.service.system.SystemPatchApplier ;
@ -71,6 +72,9 @@ public class SystemPatchApplierTest {
@Mock
private InstallScripts installScripts ;
@Mock
private DatabaseSchemaSettingsService schemaSettingsService ;
@Mock
private WidgetTypeService widgetTypeService ;
@ -373,6 +377,247 @@ public class SystemPatchApplierTest {
verify ( widgetTypeService , times ( 1 ) ) . saveWidgetType ( any ( ) ) ;
}
// --- isVersionIncreased tests ---
@ParameterizedTest ( name = "isVersionIncreased: {0} (package={1}, db={2}) -> {3}" )
@MethodSource ( "provideVersionComparisonTestCases" )
void testIsVersionIncreased ( String testName , SystemPatchApplier . VersionInfo packageVersion ,
SystemPatchApplier . VersionInfo dbVersion , boolean expected ) {
Boolean result = ReflectionTestUtils . invokeMethod ( reconciler , "isVersionIncreased" , packageVersion , dbVersion ) ;
assertEquals ( expected , result , testName ) ;
}
private static Stream < Arguments > provideVersionComparisonTestCases ( ) {
return Stream . of (
// Maintenance digit increases within same LTS family
Arguments . of ( "maintenance increased" ,
new SystemPatchApplier . VersionInfo ( 4 , 3 , 1 , 0 ) ,
new SystemPatchApplier . VersionInfo ( 4 , 3 , 0 , 0 ) , true ) ,
Arguments . of ( "maintenance increased by more than one" ,
new SystemPatchApplier . VersionInfo ( 4 , 3 , 3 , 0 ) ,
new SystemPatchApplier . VersionInfo ( 4 , 3 , 0 , 0 ) , true ) ,
// Patch digit increases within same maintenance
Arguments . of ( "patch increased" ,
new SystemPatchApplier . VersionInfo ( 4 , 3 , 0 , 1 ) ,
new SystemPatchApplier . VersionInfo ( 4 , 3 , 0 , 0 ) , true ) ,
Arguments . of ( "patch increased by more than one" ,
new SystemPatchApplier . VersionInfo ( 4 , 3 , 0 , 5 ) ,
new SystemPatchApplier . VersionInfo ( 4 , 3 , 0 , 2 ) , true ) ,
// Both maintenance and patch increased
Arguments . of ( "maintenance and patch both increased" ,
new SystemPatchApplier . VersionInfo ( 4 , 3 , 1 , 1 ) ,
new SystemPatchApplier . VersionInfo ( 4 , 3 , 0 , 0 ) , true ) ,
// Maintenance increased, patch value is lower (irrelevant — maintenance wins)
Arguments . of ( "maintenance increased, patch is lower" ,
new SystemPatchApplier . VersionInfo ( 4 , 3 , 2 , 0 ) ,
new SystemPatchApplier . VersionInfo ( 4 , 3 , 1 , 5 ) , true ) ,
// Same version — no increase
Arguments . of ( "same version" ,
new SystemPatchApplier . VersionInfo ( 4 , 3 , 0 , 0 ) ,
new SystemPatchApplier . VersionInfo ( 4 , 3 , 0 , 0 ) , false ) ,
Arguments . of ( "same version with non-zero parts" ,
new SystemPatchApplier . VersionInfo ( 4 , 3 , 1 , 2 ) ,
new SystemPatchApplier . VersionInfo ( 4 , 3 , 1 , 2 ) , false ) ,
// Decreased versions — no increase
Arguments . of ( "maintenance decreased" ,
new SystemPatchApplier . VersionInfo ( 4 , 3 , 0 , 0 ) ,
new SystemPatchApplier . VersionInfo ( 4 , 3 , 1 , 0 ) , false ) ,
Arguments . of ( "patch decreased" ,
new SystemPatchApplier . VersionInfo ( 4 , 3 , 0 , 0 ) ,
new SystemPatchApplier . VersionInfo ( 4 , 3 , 0 , 1 ) , false ) ,
// Different major — different family, skip
Arguments . of ( "different major" ,
new SystemPatchApplier . VersionInfo ( 5 , 3 , 0 , 0 ) ,
new SystemPatchApplier . VersionInfo ( 4 , 3 , 0 , 0 ) , false ) ,
Arguments . of ( "major decreased" ,
new SystemPatchApplier . VersionInfo ( 3 , 3 , 0 , 0 ) ,
new SystemPatchApplier . VersionInfo ( 4 , 3 , 0 , 0 ) , false ) ,
// Different minor — different LTS family, skip
Arguments . of ( "minor increased (different LTS family)" ,
new SystemPatchApplier . VersionInfo ( 4 , 4 , 0 , 0 ) ,
new SystemPatchApplier . VersionInfo ( 4 , 3 , 0 , 0 ) , false ) ,
Arguments . of ( "minor decreased" ,
new SystemPatchApplier . VersionInfo ( 4 , 2 , 0 , 0 ) ,
new SystemPatchApplier . VersionInfo ( 4 , 3 , 0 , 0 ) , false )
) ;
}
// --- isVersionChanged tests ---
@Test
void whenVersionIncreased_thenVersionChangedReturnsTrue ( ) {
when ( schemaSettingsService . getPackageSchemaVersion ( ) ) . thenReturn ( "4.3.1.0" ) ;
when ( schemaSettingsService . getDbSchemaVersion ( ) ) . thenReturn ( "4.3.0.0" ) ;
Boolean result = ReflectionTestUtils . invokeMethod ( reconciler , "isVersionChanged" ) ;
assertTrue ( result ) ;
}
@Test
void whenVersionNotIncreased_thenVersionChangedReturnsFalse ( ) {
when ( schemaSettingsService . getPackageSchemaVersion ( ) ) . thenReturn ( "4.3.0.0" ) ;
when ( schemaSettingsService . getDbSchemaVersion ( ) ) . thenReturn ( "4.3.0.0" ) ;
Boolean result = ReflectionTestUtils . invokeMethod ( reconciler , "isVersionChanged" ) ;
assertFalse ( result ) ;
}
@Test
void whenVersionUnparseable_thenVersionChangedReturnsFalse ( ) {
when ( schemaSettingsService . getPackageSchemaVersion ( ) ) . thenReturn ( "invalid" ) ;
when ( schemaSettingsService . getDbSchemaVersion ( ) ) . thenReturn ( "4.3.0.0" ) ;
Boolean result = ReflectionTestUtils . invokeMethod ( reconciler , "isVersionChanged" ) ;
assertFalse ( result ) ;
}
@Test
void whenDbVersionUnparseable_thenVersionChangedReturnsFalse ( ) {
when ( schemaSettingsService . getPackageSchemaVersion ( ) ) . thenReturn ( "4.3.1.0" ) ;
when ( schemaSettingsService . getDbSchemaVersion ( ) ) . thenReturn ( "bad" ) ;
Boolean result = ReflectionTestUtils . invokeMethod ( reconciler , "isVersionChanged" ) ;
assertFalse ( result ) ;
}
// --- updateLtsSqlSchema tests ---
@Test
void whenLtsSqlFileExists_thenExecutesSql ( ) throws Exception {
Path dataDir = tempDir . resolve ( "data" ) ;
Path ltsDir = dataDir . resolve ( "upgrade" ) . resolve ( "lts" ) ;
Files . createDirectories ( ltsDir ) ;
Files . writeString ( ltsDir . resolve ( "schema_update.sql" ) , "ALTER TABLE device ADD COLUMN IF NOT EXISTS test_col VARCHAR(255);" ) ;
when ( installScripts . getDataDir ( ) ) . thenReturn ( dataDir . toString ( ) ) ;
ReflectionTestUtils . invokeMethod ( reconciler , "updateLtsSqlSchema" ) ;
verify ( jdbcTemplate ) . execute ( "ALTER TABLE device ADD COLUMN IF NOT EXISTS test_col VARCHAR(255);" ) ;
}
@Test
void whenLtsSqlFileDoesNotExist_thenSkips ( ) {
Path dataDir = tempDir . resolve ( "data" ) ;
// Don't create the file
when ( installScripts . getDataDir ( ) ) . thenReturn ( dataDir . toString ( ) ) ;
ReflectionTestUtils . invokeMethod ( reconciler , "updateLtsSqlSchema" ) ;
verify ( jdbcTemplate , never ( ) ) . execute ( anyString ( ) ) ;
}
@Test
void whenLtsSqlFileHasMultipleStatements_thenExecutesAll ( ) throws Exception {
Path dataDir = tempDir . resolve ( "data" ) ;
Path ltsDir = dataDir . resolve ( "upgrade" ) . resolve ( "lts" ) ;
Files . createDirectories ( ltsDir ) ;
String sql = "DO $$ BEGIN\n" +
" IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'test_type') THEN\n" +
" CREATE TYPE test_type AS ENUM ('A', 'B');\n" +
" END IF;\n" +
"END $$;\n" +
"ALTER TABLE device ADD COLUMN IF NOT EXISTS test_col VARCHAR(255);" ;
Files . writeString ( ltsDir . resolve ( "schema_update.sql" ) , sql ) ;
when ( installScripts . getDataDir ( ) ) . thenReturn ( dataDir . toString ( ) ) ;
ReflectionTestUtils . invokeMethod ( reconciler , "updateLtsSqlSchema" ) ;
verify ( jdbcTemplate ) . execute ( sql ) ;
}
// --- applyPatchIfNeeded flow tests ---
@Test
void whenVersionIncreased_thenAppliesLtsSqlBeforeViewsAndWidgets ( ) throws Exception {
when ( schemaSettingsService . getPackageSchemaVersion ( ) ) . thenReturn ( "4.3.1.0" ) ;
when ( schemaSettingsService . getDbSchemaVersion ( ) ) . thenReturn ( "4.3.0.0" ) ;
when ( jdbcTemplate . queryForObject ( contains ( "pg_try_advisory_lock" ) , eq ( Boolean . class ) , anyLong ( ) ) ) . thenReturn ( true ) ;
when ( jdbcTemplate . queryForObject ( contains ( "pg_advisory_unlock" ) , eq ( Boolean . class ) , anyLong ( ) ) ) . thenReturn ( true ) ;
Path dataDir = tempDir . resolve ( "data" ) ;
Path ltsDir = dataDir . resolve ( "upgrade" ) . resolve ( "lts" ) ;
Files . createDirectories ( ltsDir ) ;
Files . writeString ( ltsDir . resolve ( "schema_update.sql" ) , "SELECT 1;" ) ;
when ( installScripts . getDataDir ( ) ) . thenReturn ( dataDir . toString ( ) ) ;
Path widgetTypesDir = tempDir . resolve ( "widget_types" ) ;
Files . createDirectories ( widgetTypesDir ) ;
when ( installScripts . getWidgetTypesDir ( ) ) . thenReturn ( widgetTypesDir ) ;
ReflectionTestUtils . invokeMethod ( reconciler , "applyPatchIfNeeded" ) ;
// LTS SQL was executed
verify ( jdbcTemplate ) . execute ( "SELECT 1;" ) ;
// Schema version was updated
verify ( schemaSettingsService ) . updateSchemaVersion ( ) ;
}
@Test
void whenVersionNotIncreased_thenSkipsEverything ( ) {
when ( schemaSettingsService . getPackageSchemaVersion ( ) ) . thenReturn ( "4.3.0.0" ) ;
when ( schemaSettingsService . getDbSchemaVersion ( ) ) . thenReturn ( "4.3.0.0" ) ;
ReflectionTestUtils . invokeMethod ( reconciler , "applyPatchIfNeeded" ) ;
// No lock acquired
verify ( jdbcTemplate , never ( ) ) . queryForObject ( contains ( "pg_try_advisory_lock" ) , eq ( Boolean . class ) , anyLong ( ) ) ;
// No schema update
verify ( schemaSettingsService , never ( ) ) . updateSchemaVersion ( ) ;
}
@Test
void whenLockNotAcquired_thenSkipsPatchApplication ( ) {
when ( schemaSettingsService . getPackageSchemaVersion ( ) ) . thenReturn ( "4.3.1.0" ) ;
when ( schemaSettingsService . getDbSchemaVersion ( ) ) . thenReturn ( "4.3.0.0" ) ;
when ( jdbcTemplate . queryForObject ( contains ( "pg_try_advisory_lock" ) , eq ( Boolean . class ) , anyLong ( ) ) ) . thenReturn ( false ) ;
ReflectionTestUtils . invokeMethod ( reconciler , "applyPatchIfNeeded" ) ;
verify ( schemaSettingsService , never ( ) ) . updateSchemaVersion ( ) ;
verify ( jdbcTemplate , never ( ) ) . execute ( anyString ( ) ) ;
}
@Test
void whenMaintenanceVersionIncreased_thenAppliesPatch ( ) throws Exception {
when ( schemaSettingsService . getPackageSchemaVersion ( ) ) . thenReturn ( "4.3.2.0" ) ;
when ( schemaSettingsService . getDbSchemaVersion ( ) ) . thenReturn ( "4.3.1.0" ) ;
when ( jdbcTemplate . queryForObject ( contains ( "pg_try_advisory_lock" ) , eq ( Boolean . class ) , anyLong ( ) ) ) . thenReturn ( true ) ;
when ( jdbcTemplate . queryForObject ( contains ( "pg_advisory_unlock" ) , eq ( Boolean . class ) , anyLong ( ) ) ) . thenReturn ( true ) ;
Path dataDir = tempDir . resolve ( "data" ) ;
when ( installScripts . getDataDir ( ) ) . thenReturn ( dataDir . toString ( ) ) ;
Path widgetTypesDir = tempDir . resolve ( "widget_types" ) ;
Files . createDirectories ( widgetTypesDir ) ;
when ( installScripts . getWidgetTypesDir ( ) ) . thenReturn ( widgetTypesDir ) ;
ReflectionTestUtils . invokeMethod ( reconciler , "applyPatchIfNeeded" ) ;
verify ( schemaSettingsService ) . updateSchemaVersion ( ) ;
}
@Test
void whenDifferentLtsFamily_thenSkipsPatch ( ) {
when ( schemaSettingsService . getPackageSchemaVersion ( ) ) . thenReturn ( "4.4.0.0" ) ;
when ( schemaSettingsService . getDbSchemaVersion ( ) ) . thenReturn ( "4.3.0.0" ) ;
ReflectionTestUtils . invokeMethod ( reconciler , "applyPatchIfNeeded" ) ;
verify ( jdbcTemplate , never ( ) ) . queryForObject ( contains ( "pg_try_advisory_lock" ) , eq ( Boolean . class ) , anyLong ( ) ) ;
verify ( schemaSettingsService , never ( ) ) . updateSchemaVersion ( ) ;
}
private static Stream < Arguments > provideDescriptorComparisonTestCases ( ) {
return Stream . of (
Arguments . of ( "Both null" , null , null , true ) ,