From ee541633527b6146d7145009ad5e71c9e9c1b962 Mon Sep 17 00:00:00 2001 From: Oleksandra Matviienko Date: Tue, 5 May 2026 15:05:57 +0200 Subject: [PATCH] Skipped image field when comparing widgets bundle metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ImageService.replaceBase64WithImageUrl rewrites base64 data URIs into system-image URLs at save time. After install, the DB image is a 'tb-image;...' URL while the JSON file still carries a base64 data URI — naive string compare always reported a diff and caused every system bundle to be re-saved on every patch run. --- .../service/system/SystemPatchApplier.java | 5 +++- .../server/system/SystemPatchApplierTest.java | 27 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java b/application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java index e5bc65a047..a256888107 100644 --- a/application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java +++ b/application/src/main/java/org/thingsboard/server/service/system/SystemPatchApplier.java @@ -316,9 +316,12 @@ public class SystemPatchApplier { } private boolean isWidgetsBundleChanged(WidgetsBundle existing, WidgetsBundle file) { + // Image is intentionally NOT compared: the file always carries a base64 data URI, while the DB stores + // the system-image URL produced by ImageService.replaceBase64WithImageUrl on save. A naive string compare + // would always report a diff and re-save every system bundle on every patch run. Image content changes + // are out of scope for the patch applier — full reinstall covers them. return !Objects.equals(existing.getTitle(), file.getTitle()) || !Objects.equals(existing.getDescription(), file.getDescription()) - || !Objects.equals(existing.getImage(), file.getImage()) || !Objects.equals(existing.getOrder(), file.getOrder()) || existing.isScada() != file.isScada(); } diff --git a/application/src/test/java/org/thingsboard/server/system/SystemPatchApplierTest.java b/application/src/test/java/org/thingsboard/server/system/SystemPatchApplierTest.java index 4275b27560..f2485dc7d5 100644 --- a/application/src/test/java/org/thingsboard/server/system/SystemPatchApplierTest.java +++ b/application/src/test/java/org/thingsboard/server/system/SystemPatchApplierTest.java @@ -1010,6 +1010,33 @@ public class SystemPatchApplierTest { verify(widgetTypeService, never()).updateWidgetsBundleWidgetFqns(any(), any(), any()); } + @Test + void whenOnlyBundleImageFormatDiffers_thenNoUpdate() throws Exception { + Path bundlesDir = tempDir.resolve("widget_bundles"); + Files.createDirectories(bundlesDir); + when(installScripts.getWidgetBundlesDir()).thenReturn(bundlesDir); + + // File carries a base64 data URI; DB has the resolved system-image URL — same content, different format. + Files.writeString(bundlesDir.resolve("charts.json"), + "{\"widgetsBundle\":{\"alias\":\"charts\",\"title\":\"Charts\",\"description\":\"d\",\"order\":10," + + "\"image\":\"data:image/png;base64,iVBORw0KGgo\"}," + + "\"widgetTypeFqns\":[]}"); + + WidgetsBundle existingBundle = createTestBundle("charts", "Charts"); + existingBundle.setDescription("d"); + existingBundle.setOrder(10); + existingBundle.setImage("tb-image;/api/images/system/charts.png"); + when(widgetsBundleService.findWidgetsBundleByTenantIdAndAlias(TenantId.SYS_TENANT_ID, "charts")).thenReturn(existingBundle); + when(widgetTypeService.findWidgetFqnsByWidgetsBundleId(TenantId.SYS_TENANT_ID, existingBundle.getId())) + .thenReturn(List.of()); + + Integer updated = ReflectionTestUtils.invokeMethod(reconciler, "updateWidgetBundles"); + + assertEquals(0, updated); + verify(widgetsBundleService, never()).saveWidgetsBundle(any()); + verify(widgetTypeService, never()).updateWidgetsBundleWidgetFqns(any(), any(), any()); + } + @Test void whenBundleMetadataChanged_thenUpdateBundle() throws Exception { Path bundlesDir = tempDir.resolve("widget_bundles");