Skip to content

Commit d66adbd

Browse files
fix: Pass valid date for setting value during CDSR sync [DHIS2-21781](2.43) (#24483)
1 parent 94b9858 commit d66adbd

3 files changed

Lines changed: 122 additions & 4 deletions

File tree

dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/sync/CompleteDataSetRegistrationSynchronization.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,16 +102,14 @@ public SynchronizationResult synchronizeData(JobProgress progress) {
102102
this::createContext);
103103

104104
if (context.getObjectsToSynchronize() == 0) {
105-
settingsService.put(
106-
"keyLastCompleteDataSetRegistrationSyncSuccess", context.getStartTime().toString());
105+
settingsService.put("keyLastCompleteDataSetRegistrationSyncSuccess", context.getStartTime());
107106
String msg = "Skipping completeness synchronization, no new or updated data";
108107
progress.completedProcess(msg);
109108
return SynchronizationResult.success(msg);
110109
}
111110

112111
if (runSync(context, progress)) {
113-
settingsService.put(
114-
"keyLastCompleteDataSetRegistrationSyncSuccess", context.getStartTime().toString());
112+
settingsService.put("keyLastCompleteDataSetRegistrationSyncSuccess", context.getStartTime());
115113
String msg = "Complete data set registration synchronization is done.";
116114
progress.completedProcess(msg);
117115
return SynchronizationResult.success(msg);
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright (c) 2004-2026, University of Oslo
3+
* All rights reserved.
4+
*
5+
* Redistribution and use in source and binary forms, with or without
6+
* modification, are permitted provided that the following conditions are met:
7+
*
8+
* 1. Redistributions of source code must retain the above copyright notice, this
9+
* list of conditions and the following disclaimer.
10+
*
11+
* 2. Redistributions in binary form must reproduce the above copyright notice,
12+
* this list of conditions and the following disclaimer in the documentation
13+
* and/or other materials provided with the distribution.
14+
*
15+
* 3. Neither the name of the copyright holder nor the names of its contributors
16+
* may be used to endorse or promote products derived from this software without
17+
* specific prior written permission.
18+
*
19+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
20+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
23+
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24+
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25+
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
26+
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28+
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
package org.hisp.dhis.dxf2.sync;
31+
32+
import static org.junit.jupiter.api.Assertions.assertTrue;
33+
import static org.mockito.ArgumentMatchers.any;
34+
import static org.mockito.ArgumentMatchers.anyString;
35+
import static org.mockito.ArgumentMatchers.eq;
36+
import static org.mockito.Mockito.verify;
37+
import static org.mockito.Mockito.when;
38+
39+
import java.io.Serializable;
40+
import java.util.Map;
41+
import org.hisp.dhis.dataset.CompleteDataSetRegistrationService;
42+
import org.hisp.dhis.dxf2.dataset.CompleteDataSetRegistrationExchangeService;
43+
import org.hisp.dhis.scheduling.JobProgress;
44+
import org.hisp.dhis.setting.Settings;
45+
import org.hisp.dhis.setting.SystemSettings;
46+
import org.hisp.dhis.setting.SystemSettingsService;
47+
import org.junit.jupiter.api.Test;
48+
import org.junit.jupiter.api.extension.ExtendWith;
49+
import org.mockito.ArgumentCaptor;
50+
import org.mockito.Captor;
51+
import org.mockito.Mock;
52+
import org.mockito.junit.jupiter.MockitoExtension;
53+
import org.springframework.http.HttpMethod;
54+
import org.springframework.http.HttpStatus;
55+
import org.springframework.http.ResponseEntity;
56+
import org.springframework.web.client.RestTemplate;
57+
58+
/**
59+
* Tests {@link CompleteDataSetRegistrationSynchronization}.
60+
*
61+
* <p>Regression test for {@code BadRequestException: Not a valid value for setting
62+
* keyLastCompleteDataSetRegistrationSyncSuccess} thrown during sync process. The value handed to
63+
* {@link SystemSettingsService#put} must be a persistable setting value, i.e. round-trip through
64+
* {@link Settings#valueOf} into a value the settings validation accepts.
65+
*/
66+
@ExtendWith(MockitoExtension.class)
67+
class CompleteDataSetRegistrationSyncTest {
68+
69+
private static final String SYNC_SUCCESS_KEY = "keyLastCompleteDataSetRegistrationSyncSuccess";
70+
71+
@Mock private SystemSettingsService settingsService;
72+
@Mock private RestTemplate restTemplate;
73+
@Mock private CompleteDataSetRegistrationService completeDataSetRegistrationService;
74+
75+
@Mock
76+
private CompleteDataSetRegistrationExchangeService completeDataSetRegistrationExchangeService;
77+
78+
@Captor private ArgumentCaptor<Serializable> settingValueCaptor;
79+
80+
@Test
81+
void storesAPersistableTimestampWhenNothingToSynchronize() {
82+
// remote server configured and available, and no complete data sets to synchronize
83+
SystemSettings settings =
84+
SystemSettings.of(
85+
Map.of(
86+
"keyRemoteInstanceUrl", "http://remote.example.org",
87+
"keyRemoteInstanceUsername", "admin",
88+
"keyRemoteInstancePassword", "district"));
89+
when(settingsService.getCurrentSettings()).thenReturn(settings);
90+
when(restTemplate.exchange(anyString(), eq(HttpMethod.GET), any(), eq(String.class)))
91+
.thenReturn(new ResponseEntity<>("pong", HttpStatus.OK));
92+
when(completeDataSetRegistrationService.getCompleteDataSetCountLastUpdatedAfter(any()))
93+
.thenReturn(0);
94+
95+
CompleteDataSetRegistrationSynchronization sync =
96+
new CompleteDataSetRegistrationSynchronization(
97+
settingsService,
98+
restTemplate,
99+
completeDataSetRegistrationService,
100+
completeDataSetRegistrationExchangeService);
101+
102+
JobProgress progress = JobProgress.noop();
103+
sync.synchronizeData(progress);
104+
105+
// the "last success" timestamp must be recorded ...
106+
verify(settingsService).put(eq(SYNC_SUCCESS_KEY), settingValueCaptor.capture());
107+
// ... as a value the settings layer can actually persist (this is what put() does internally).
108+
// Passing Date.toString() produces an unparseable value and would fail validation here.
109+
Serializable stored = settingValueCaptor.getValue();
110+
assertTrue(
111+
SystemSettings.of(Map.of()).isValid(SYNC_SUCCESS_KEY, Settings.valueOf(stored)),
112+
"value handed to the settings service must be a persistable setting value, but was: "
113+
+ stored);
114+
}
115+
}

dhis-2/dhis-services/dhis-service-setting/src/test/java/org/hisp/dhis/setting/SystemSettingsTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,11 @@ void testIsValid_Date() {
257257
assertTrue(settings.isValid("keyLastMonitoringRun", date));
258258
assertFalse(settings.isValid("keyLastMonitoringRun", "hello"));
259259
assertFalse(settings.isValid("keyLastMonitoringRun", "true"));
260+
// the correctly serialized form of a Date is accepted ...
261+
assertTrue(settings.isValid("keyLastMonitoringRun", Settings.valueOf(new Date())));
262+
// ... but Date.toString() output is NOT
263+
assertFalse(settings.isValid("keyLastMonitoringRun", new Date().toString()));
264+
assertFalse(settings.isValid("keyLastMonitoringRun", "Mon Jun 08 07:50:29 IST 2026"));
260265
}
261266

262267
@Test

0 commit comments

Comments
 (0)