@ -32,6 +32,7 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.id.TenantId ;
import org.thingsboard.server.common.data.id.TenantId ;
import org.thingsboard.server.common.data.id.WidgetTypeId ;
import org.thingsboard.server.common.data.id.WidgetTypeId ;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails ;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails ;
import org.thingsboard.server.dao.resource.ImageService ;
import org.thingsboard.server.dao.widget.WidgetTypeService ;
import org.thingsboard.server.dao.widget.WidgetTypeService ;
import org.thingsboard.server.service.install.DatabaseSchemaSettingsService ;
import org.thingsboard.server.service.install.DatabaseSchemaSettingsService ;
import org.thingsboard.server.service.install.InstallScripts ;
import org.thingsboard.server.service.install.InstallScripts ;
@ -39,6 +40,8 @@ import org.thingsboard.server.service.system.SystemPatchApplier;
import java.nio.file.Files ;
import java.nio.file.Files ;
import java.nio.file.Path ;
import java.nio.file.Path ;
import java.util.Collections ;
import java.util.Set ;
import java.util.UUID ;
import java.util.UUID ;
import java.util.concurrent.CountDownLatch ;
import java.util.concurrent.CountDownLatch ;
import java.util.concurrent.TimeUnit ;
import java.util.concurrent.TimeUnit ;
@ -78,6 +81,9 @@ public class SystemPatchApplierTest {
@Mock
@Mock
private WidgetTypeService widgetTypeService ;
private WidgetTypeService widgetTypeService ;
@Mock
private ImageService imageService ;
@InjectMocks
@InjectMocks
private SystemPatchApplier reconciler ;
private SystemPatchApplier reconciler ;
@ -652,4 +658,200 @@ public class SystemPatchApplierTest {
return widget ;
return widget ;
}
}
// --- createMissingSystemImages tests ---
@Test
void whenImagesDirDoesNotExist_thenReturnsZeroAndDoesNotCallImageService ( ) {
Path dataDir = tempDir . resolve ( "data" ) ;
// Intentionally do not create resources/images dir
when ( installScripts . getDataDir ( ) ) . thenReturn ( dataDir . toString ( ) ) ;
Integer created = ReflectionTestUtils . invokeMethod ( reconciler , "createMissingSystemImages" ) ;
assertEquals ( 0 , created ) ;
verify ( imageService , never ( ) ) . getAllImageKeysByTenantId ( any ( ) ) ;
verify ( imageService , never ( ) ) . createOrUpdateSystemImage ( anyString ( ) , any ( byte [ ] . class ) ) ;
}
@Test
void whenImagesDirIsEmpty_thenReturnsZeroAndDoesNotCallImageService ( ) throws Exception {
Path imagesDir = tempDir . resolve ( "data" ) . resolve ( InstallScripts . RESOURCES_DIR ) . resolve ( "images" ) ;
Files . createDirectories ( imagesDir ) ;
when ( installScripts . getDataDir ( ) ) . thenReturn ( tempDir . resolve ( "data" ) . toString ( ) ) ;
when ( imageService . getAllImageKeysByTenantId ( TenantId . SYS_TENANT_ID ) ) . thenReturn ( Collections . emptySet ( ) ) ;
Integer created = ReflectionTestUtils . invokeMethod ( reconciler , "createMissingSystemImages" ) ;
assertEquals ( 0 , created ) ;
verify ( imageService , never ( ) ) . createOrUpdateSystemImage ( anyString ( ) , any ( byte [ ] . class ) ) ;
}
@Test
void whenSystemImageDoesNotExistInDb_thenCreateIt ( ) throws Exception {
Path imagesDir = tempDir . resolve ( "data" ) . resolve ( InstallScripts . RESOURCES_DIR ) . resolve ( "images" ) ;
Files . createDirectories ( imagesDir ) ;
when ( installScripts . getDataDir ( ) ) . thenReturn ( tempDir . resolve ( "data" ) . toString ( ) ) ;
byte [ ] imageBytes = new byte [ ] { 1 , 2 , 3 , 4 , 5 } ;
Files . write ( imagesDir . resolve ( "gateway.png" ) , imageBytes ) ;
when ( imageService . getAllImageKeysByTenantId ( TenantId . SYS_TENANT_ID ) ) . thenReturn ( Collections . emptySet ( ) ) ;
Integer created = ReflectionTestUtils . invokeMethod ( reconciler , "createMissingSystemImages" ) ;
assertEquals ( 1 , created ) ;
verify ( imageService ) . getAllImageKeysByTenantId ( TenantId . SYS_TENANT_ID ) ;
verify ( imageService ) . createOrUpdateSystemImage ( eq ( "gateway.png" ) , eq ( imageBytes ) ) ;
}
@Test
void whenSystemImageExistsInDb_thenSkipIt ( ) throws Exception {
Path imagesDir = tempDir . resolve ( "data" ) . resolve ( InstallScripts . RESOURCES_DIR ) . resolve ( "images" ) ;
Files . createDirectories ( imagesDir ) ;
when ( installScripts . getDataDir ( ) ) . thenReturn ( tempDir . resolve ( "data" ) . toString ( ) ) ;
Files . write ( imagesDir . resolve ( "gateway.png" ) , new byte [ ] { 1 , 2 , 3 } ) ;
when ( imageService . getAllImageKeysByTenantId ( TenantId . SYS_TENANT_ID ) ) . thenReturn ( Set . of ( "gateway.png" ) ) ;
Integer created = ReflectionTestUtils . invokeMethod ( reconciler , "createMissingSystemImages" ) ;
assertEquals ( 0 , created ) ;
verify ( imageService ) . getAllImageKeysByTenantId ( TenantId . SYS_TENANT_ID ) ;
verify ( imageService , never ( ) ) . createOrUpdateSystemImage ( anyString ( ) , any ( byte [ ] . class ) ) ;
}
@Test
void whenMixOfNewAndExistingImages_thenOnlyCreateMissingOnes ( ) throws Exception {
Path imagesDir = tempDir . resolve ( "data" ) . resolve ( InstallScripts . RESOURCES_DIR ) . resolve ( "images" ) ;
Files . createDirectories ( imagesDir ) ;
when ( installScripts . getDataDir ( ) ) . thenReturn ( tempDir . resolve ( "data" ) . toString ( ) ) ;
byte [ ] newImageBytes = new byte [ ] { 9 , 9 , 9 } ;
byte [ ] existingImageBytes = new byte [ ] { 1 , 1 , 1 } ;
Files . write ( imagesDir . resolve ( "new.png" ) , newImageBytes ) ;
Files . write ( imagesDir . resolve ( "existing.svg" ) , existingImageBytes ) ;
when ( imageService . getAllImageKeysByTenantId ( TenantId . SYS_TENANT_ID ) ) . thenReturn ( Set . of ( "existing.svg" ) ) ;
Integer created = ReflectionTestUtils . invokeMethod ( reconciler , "createMissingSystemImages" ) ;
assertEquals ( 1 , created ) ;
verify ( imageService , times ( 1 ) ) . getAllImageKeysByTenantId ( TenantId . SYS_TENANT_ID ) ;
verify ( imageService ) . createOrUpdateSystemImage ( eq ( "new.png" ) , eq ( newImageBytes ) ) ;
verify ( imageService , never ( ) ) . createOrUpdateSystemImage ( eq ( "existing.svg" ) , any ( byte [ ] . class ) ) ;
}
@Test
void whenImagesDirContainsSubdirectory_thenSubdirectoryIsIgnored ( ) throws Exception {
Path imagesDir = tempDir . resolve ( "data" ) . resolve ( InstallScripts . RESOURCES_DIR ) . resolve ( "images" ) ;
Files . createDirectories ( imagesDir ) ;
Files . createDirectories ( imagesDir . resolve ( "nested" ) ) ;
when ( installScripts . getDataDir ( ) ) . thenReturn ( tempDir . resolve ( "data" ) . toString ( ) ) ;
byte [ ] imageBytes = new byte [ ] { 5 , 6 , 7 } ;
Files . write ( imagesDir . resolve ( "logo.png" ) , imageBytes ) ;
when ( imageService . getAllImageKeysByTenantId ( TenantId . SYS_TENANT_ID ) ) . thenReturn ( Collections . emptySet ( ) ) ;
Integer created = ReflectionTestUtils . invokeMethod ( reconciler , "createMissingSystemImages" ) ;
assertEquals ( 1 , created ) ;
verify ( imageService ) . createOrUpdateSystemImage ( eq ( "logo.png" ) , eq ( imageBytes ) ) ;
verify ( imageService , never ( ) ) . createOrUpdateSystemImage ( eq ( "nested" ) , any ( byte [ ] . class ) ) ;
}
@Test
void whenMultipleNewImages_thenCreatesAll ( ) throws Exception {
Path imagesDir = tempDir . resolve ( "data" ) . resolve ( InstallScripts . RESOURCES_DIR ) . resolve ( "images" ) ;
Files . createDirectories ( imagesDir ) ;
when ( installScripts . getDataDir ( ) ) . thenReturn ( tempDir . resolve ( "data" ) . toString ( ) ) ;
Files . write ( imagesDir . resolve ( "a.png" ) , new byte [ ] { 1 } ) ;
Files . write ( imagesDir . resolve ( "b.svg" ) , new byte [ ] { 2 } ) ;
Files . write ( imagesDir . resolve ( "c.jpg" ) , new byte [ ] { 3 } ) ;
when ( imageService . getAllImageKeysByTenantId ( TenantId . SYS_TENANT_ID ) ) . thenReturn ( Collections . emptySet ( ) ) ;
Integer created = ReflectionTestUtils . invokeMethod ( reconciler , "createMissingSystemImages" ) ;
assertEquals ( 3 , created ) ;
verify ( imageService , times ( 1 ) ) . getAllImageKeysByTenantId ( TenantId . SYS_TENANT_ID ) ;
verify ( imageService ) . createOrUpdateSystemImage ( eq ( "a.png" ) , any ( byte [ ] . class ) ) ;
verify ( imageService ) . createOrUpdateSystemImage ( eq ( "b.svg" ) , any ( byte [ ] . class ) ) ;
verify ( imageService ) . createOrUpdateSystemImage ( eq ( "c.jpg" ) , any ( byte [ ] . class ) ) ;
}
@Test
void whenImageServiceThrows_thenWrapsAndPropagates ( ) throws Exception {
Path imagesDir = tempDir . resolve ( "data" ) . resolve ( InstallScripts . RESOURCES_DIR ) . resolve ( "images" ) ;
Files . createDirectories ( imagesDir ) ;
when ( installScripts . getDataDir ( ) ) . thenReturn ( tempDir . resolve ( "data" ) . toString ( ) ) ;
Files . write ( imagesDir . resolve ( "broken.png" ) , new byte [ ] { 1 , 2 } ) ;
when ( imageService . getAllImageKeysByTenantId ( TenantId . SYS_TENANT_ID ) ) . thenReturn ( Collections . emptySet ( ) ) ;
when ( imageService . createOrUpdateSystemImage ( eq ( "broken.png" ) , any ( byte [ ] . class ) ) )
. thenThrow ( new RuntimeException ( "DB error" ) ) ;
RuntimeException thrown = assertThrows ( RuntimeException . class ,
( ) - > ReflectionTestUtils . invokeMethod ( reconciler , "createMissingSystemImages" ) ) ;
assertTrue ( thrown . getMessage ( ) . contains ( "broken.png" ) ) ;
}
@Test
void whenExistingKeysLookupFails_thenDoesNotCreateImage ( ) throws Exception {
Path imagesDir = tempDir . resolve ( "data" ) . resolve ( InstallScripts . RESOURCES_DIR ) . resolve ( "images" ) ;
Files . createDirectories ( imagesDir ) ;
when ( installScripts . getDataDir ( ) ) . thenReturn ( tempDir . resolve ( "data" ) . toString ( ) ) ;
Files . write ( imagesDir . resolve ( "img.png" ) , new byte [ ] { 1 } ) ;
when ( imageService . getAllImageKeysByTenantId ( TenantId . SYS_TENANT_ID ) )
. thenThrow ( new RuntimeException ( "lookup failed" ) ) ;
assertThrows ( RuntimeException . class ,
( ) - > ReflectionTestUtils . invokeMethod ( reconciler , "createMissingSystemImages" ) ) ;
verify ( imageService , never ( ) ) . createOrUpdateSystemImage ( anyString ( ) , any ( byte [ ] . class ) ) ;
}
// --- applyPatchIfNeeded integration with createMissingSystemImages ---
@Test
void whenApplyPatchIfNeededRuns_thenCreatesMissingImagesAfterWidgets ( ) 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 imagesDir = dataDir . resolve ( InstallScripts . RESOURCES_DIR ) . resolve ( "images" ) ;
Files . createDirectories ( imagesDir ) ;
byte [ ] imgBytes = new byte [ ] { 7 , 7 , 7 } ;
Files . write ( imagesDir . resolve ( "new_icon.svg" ) , imgBytes ) ;
when ( installScripts . getDataDir ( ) ) . thenReturn ( dataDir . toString ( ) ) ;
Path widgetTypesDir = tempDir . resolve ( "widget_types" ) ;
Files . createDirectories ( widgetTypesDir ) ;
when ( installScripts . getWidgetTypesDir ( ) ) . thenReturn ( widgetTypesDir ) ;
when ( imageService . getAllImageKeysByTenantId ( TenantId . SYS_TENANT_ID ) ) . thenReturn ( Collections . emptySet ( ) ) ;
ReflectionTestUtils . invokeMethod ( reconciler , "applyPatchIfNeeded" ) ;
verify ( imageService ) . createOrUpdateSystemImage ( eq ( "new_icon.svg" ) , eq ( imgBytes ) ) ;
verify ( schemaSettingsService ) . updateSchemaVersion ( ) ;
}
@Test
void whenVersionNotIncreased_thenImagesAreNotTouched ( ) {
when ( schemaSettingsService . getPackageSchemaVersion ( ) ) . thenReturn ( "4.3.0.0" ) ;
when ( schemaSettingsService . getDbSchemaVersion ( ) ) . thenReturn ( "4.3.0.0" ) ;
ReflectionTestUtils . invokeMethod ( reconciler , "applyPatchIfNeeded" ) ;
verify ( imageService , never ( ) ) . getAllImageKeysByTenantId ( any ( ) ) ;
verify ( imageService , never ( ) ) . createOrUpdateSystemImage ( anyString ( ) , any ( byte [ ] . class ) ) ;
}
}
}