diff --git a/src/main/java/org/medicmobile/webapp/mobile/AppNotificationManager.java b/src/main/java/org/medicmobile/webapp/mobile/AppNotificationManager.java index 8e204111..6ce50119 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/AppNotificationManager.java +++ b/src/main/java/org/medicmobile/webapp/mobile/AppNotificationManager.java @@ -35,9 +35,9 @@ public class AppNotificationManager { private static final String CHANNEL_NAME = "CHT Android Notifications"; public static final int REQUEST_NOTIFICATION_PERMISSION = 1001; public static final String TASK_NOTIFICATIONS_KEY = "task_notifications"; + public static final String TASK_NOTIFICATION_SETTINGS_KEY = "task_notification_settings"; public static final String TASK_NOTIFICATION_DAY_KEY = "cht_task_notification_day"; public static final String LATEST_NOTIFICATION_TIMESTAMP_KEY = "cht_task_notification_timestamp"; - public static final String MAX_NOTIFICATIONS_TO_SHOW_KEY = "cht_max_task_notifications"; private final Context context; private final NotificationManager manager; @@ -114,7 +114,7 @@ public void showNotificationsFromJsArray(String jsArrayString) throws JSONExcept private void showMultipleTaskNotifications(JSONArray dataArray) throws JSONException { Intent intent = new Intent(context, EmbeddedBrowserActivity.class); intent.setData(Uri.parse(TextUtils.concat(appUrl, "/#/tasks").toString())); - long maxNotifications = appDataStore.getLongBlocking(MAX_NOTIFICATIONS_TO_SHOW_KEY, 8L); + long maxNotifications = getSettings(appDataStore).getLong("maxNotifications"); long latestStoredTimestamp = getLatestStoredTimestamp(getStartOfDay()); int counter = 0; long latestReadyAt = 0; @@ -148,9 +148,15 @@ public void saveLatestNotificationTimestamp(long value) { appDataStore.saveLong(LATEST_NOTIFICATION_TIMESTAMP_KEY, value); } + public static JSONObject getSettings(AppDataStore dataStore){ + String settings = dataStore.getStringBlocking(TASK_NOTIFICATION_SETTINGS_KEY, "{}"); + return Utils.parseJSONObject(settings); + } + public long getStartOfDay() { - return LocalDate.now() - .atStartOfDay(ZoneId.systemDefault()) + ZoneId zone = ZoneId.systemDefault(); + return LocalDate.now(zone) + .atStartOfDay(zone) .toInstant().toEpochMilli(); } diff --git a/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java b/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java index 88f8453b..56d847f2 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java +++ b/src/main/java/org/medicmobile/webapp/mobile/MedicAndroidJavascript.java @@ -167,11 +167,18 @@ public void mrdt_verify() { } } + //CHT-Core v5.1 and v5.2 use this @JavascriptInterface public void updateTaskNotificationStore(String notifications, long maxNotifications) { + String settings = String.format("{maxNotifications: %s}", maxNotifications); + updateTaskNotificationStoreWithSettings(notifications, settings); + } + + @JavascriptInterface + public void updateTaskNotificationStoreWithSettings( + String notifications, String settings) { AppDataStore appDataStore = AppDataStore.getInstance(parent.getApplicationContext()); - appDataStore.saveLong(AppNotificationManager.MAX_NOTIFICATIONS_TO_SHOW_KEY, maxNotifications); - appDataStore.saveString(AppNotificationManager.TASK_NOTIFICATIONS_KEY, notifications); + appDataStore.saveTaskNotificationSettingsBlocking(settings, notifications); } @android.webkit.JavascriptInterface diff --git a/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java b/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java index c56f94a7..243eeb14 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java +++ b/src/main/java/org/medicmobile/webapp/mobile/NotificationWorker.java @@ -9,8 +9,12 @@ import androidx.work.WorkerParameters; import org.json.JSONException; +import org.json.JSONObject; import org.medicmobile.webapp.mobile.util.AppDataStore; +import java.time.LocalTime; +import java.time.ZoneId; + public class NotificationWorker extends Worker { public static final String NOTIFICATION_WORK_REQUEST_TAG = "cht_notification_tag"; public static final String NOTIFICATION_WORK_NAME = "appNotifications"; @@ -27,13 +31,29 @@ public Result doWork() { AppDataStore appDataStore = AppDataStore.getInstance(context); AppNotificationManager appNotificationManager = new AppNotificationManager(context); try { - String result = appDataStore - .getStringBlocking(AppNotificationManager.TASK_NOTIFICATIONS_KEY, "[]"); - appNotificationManager.showNotificationsFromJsArray(result); + JSONObject notificationWindowSettings = AppNotificationManager.getSettings(appDataStore); + if (isNotificationWindow(notificationWindowSettings)) { + String notifications = appDataStore + .getStringBlocking(AppNotificationManager.TASK_NOTIFICATIONS_KEY, "[]"); + appNotificationManager.showNotificationsFromJsArray(notifications); + } return Result.success(); } catch (JSONException e) { log(e, "error showing notifications"); return Result.failure(); } } + + boolean isNotificationWindow(JSONObject settings) throws JSONException { + if (!settings.has("start") || !settings.has("end")) { + return true; + } + LocalTime start = Utils.formatTime(settings.getString("start")); + LocalTime end = Utils.formatTime(settings.getString("end")); + if (start == null || end == null) { + return false; + } + LocalTime now = LocalTime.now(ZoneId.systemDefault()); + return now.isAfter(start) && now.isBefore(end); + } } diff --git a/src/main/java/org/medicmobile/webapp/mobile/Utils.java b/src/main/java/org/medicmobile/webapp/mobile/Utils.java index daaa4df8..22f2513a 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/Utils.java +++ b/src/main/java/org/medicmobile/webapp/mobile/Utils.java @@ -20,11 +20,14 @@ import org.json.JSONObject; import java.io.File; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; import java.util.Map; import java.util.Optional; final class Utils { - private Utils() {} + private Utils() { + } /** * @see #isValidNavigationUrl(String, String) @@ -57,17 +60,17 @@ static boolean isUrlRelated(String appUrl, String uriToTest) { */ static boolean isValidNavigationUrl(String appUrl, String navUrl) { boolean isValid = isUrlRelated(appUrl, navUrl); - if (isValid && !navUrl.matches(".*/(login|_rewrite).*")) { + if (isValid && !navUrl.matches(".*/(login|_rewrite).*")) { // NOSONAR return true; } return false; } static JSONObject json(Object... keyVals) throws JSONException { - if(DEBUG && keyVals.length % 2 != 0) throw new AssertionError(); + if (DEBUG && keyVals.length % 2 != 0) throw new AssertionError(); JSONObject o = new JSONObject(); - for(int i=keyVals.length-1; i>0; i-=2) { - o.put(keyVals[i-1].toString(), keyVals[i]); + for (int i = keyVals.length - 1; i > 0; i -= 2) { + o.put(keyVals[i - 1].toString(), keyVals[i]); } return o; } @@ -77,7 +80,7 @@ static boolean intentHandlerAvailableFor(Context ctx, Intent intent) { } static void startAppActivityChain(Activity a) { - if(SettingsStore.in(a).hasWebappSettings()) { + if (SettingsStore.in(a).hasWebappSettings()) { a.startActivity(new Intent(a, EmbeddedBrowserActivity.class)); } else { a.startActivity(new Intent(a, SettingsDialogActivity.class)); @@ -86,7 +89,7 @@ static void startAppActivityChain(Activity a) { } static String createUseragentFrom(String current) { - if(current.contains(APPLICATION_ID)) return current; + if (current.contains(APPLICATION_ID)) return current; return String.format("%s %s/%s", current, APPLICATION_ID, VERSION_NAME); @@ -101,6 +104,7 @@ static void restartApp(Context context) { /** * The file path can be a regular file or a content ("content://" scheme) + * * @param path {String} File path * @return {Uri} */ @@ -128,6 +132,7 @@ static boolean isDebug() { /** * parses js string array to JSONArray + * * @param stringArray {String} array in string format * @return {JSONArray} */ @@ -140,6 +145,26 @@ static JSONArray parseJSArrayData(String stringArray) { } } + static JSONObject parseJSONObject(String inputObject) { + try { + return new JSONObject(inputObject); + } catch (JSONException e) { + log(e, "error parsing object"); + return new JSONObject(); + } + } + + //formats string time to HH:mm + static LocalTime formatTime(String timeString) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm"); + try { + return LocalTime.parse(timeString, formatter); + } catch (Exception e) { + log(e, "Error parsing window time"); + return null; + } + } + static boolean checkIfDomainsAreVerified(Context context) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { return true; diff --git a/src/main/java/org/medicmobile/webapp/mobile/util/AppDataStore.java b/src/main/java/org/medicmobile/webapp/mobile/util/AppDataStore.java index eeddc2b9..fca126c4 100644 --- a/src/main/java/org/medicmobile/webapp/mobile/util/AppDataStore.java +++ b/src/main/java/org/medicmobile/webapp/mobile/util/AppDataStore.java @@ -14,11 +14,16 @@ import androidx.datastore.preferences.rxjava3.RxPreferenceDataStoreBuilder; import androidx.datastore.rxjava3.RxDataStore; +import org.medicmobile.webapp.mobile.AppNotificationManager; + +import java.util.concurrent.TimeUnit; + import io.reactivex.rxjava3.core.Single; @OptIn(markerClass = kotlinx.coroutines.ExperimentalCoroutinesApi.class) public class AppDataStore { private static final String DATASTORE_NAME = "cht_datastore"; + private static final long WRITE_TIMEOUT_SECONDS = 5; private static AppDataStore instance; private final RxDataStore dataStore; @@ -60,6 +65,11 @@ public void saveLongBlocking(String key, Long value) { Preferences ignored = updateResult.blockingGet(); // NOSONAR } + public void saveStringBlocking(String key, String value) { + Single updateResult = save(PreferencesKeys.stringKey(key), value); + Preferences ignored = updateResult.blockingGet(); // NOSONAR + } + private T getBlocking(Key key, @Nullable T defaultValue) { return dataStore .data() @@ -77,4 +87,25 @@ public String getStringBlocking(String key, @Nullable String defaultValue) { public long getLongBlocking(String key, @Nullable Long defaultValue) { return getBlocking(PreferencesKeys.longKey(key), defaultValue); } + + /** + * Persists the task-notifications, settings and max-count in a single atomic transaction. + */ + public void saveTaskNotificationSettingsBlocking(String settings, String notifications) { + try { + Preferences ignored = dataStore // NOSONAR + .updateDataAsync(preferences -> { + MutablePreferences mutablePreferences = preferences.toMutablePreferences(); + mutablePreferences.set(PreferencesKeys + .stringKey(AppNotificationManager.TASK_NOTIFICATION_SETTINGS_KEY), settings); + mutablePreferences.set(PreferencesKeys + .stringKey(AppNotificationManager.TASK_NOTIFICATIONS_KEY), notifications); + return Single.just(mutablePreferences); + }) + .timeout(WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .blockingGet(); + } catch (Exception e) { + log(e, "AppDataStore :: saving task notification settings failed/timed out"); + } + } } diff --git a/src/test/java/org/medicmobile/webapp/mobile/AppNotificationManagerTest.java b/src/test/java/org/medicmobile/webapp/mobile/AppNotificationManagerTest.java index 77321ffc..162f0bad 100644 --- a/src/test/java/org/medicmobile/webapp/mobile/AppNotificationManagerTest.java +++ b/src/test/java/org/medicmobile/webapp/mobile/AppNotificationManagerTest.java @@ -38,6 +38,7 @@ public void setup() { appNotificationManager = spy(new AppNotificationManager(context)); startOfDay = appNotificationManager.getStartOfDay(); appDataStore = AppDataStore.getInstance(context); + appDataStore.saveStringBlocking(AppNotificationManager.TASK_NOTIFICATION_SETTINGS_KEY, "{\"maxNotifications\": 8}"); useBlockingDataStoreGet(); } @@ -46,7 +47,7 @@ public void resetDataStore() { appDataStore.saveString(AppNotificationManager.TASK_NOTIFICATIONS_KEY, "[]"); appDataStore.saveLong(AppNotificationManager.TASK_NOTIFICATION_DAY_KEY, 0L); appDataStore.saveLong(AppNotificationManager.LATEST_NOTIFICATION_TIMESTAMP_KEY, 0L); - appDataStore.saveLong(AppNotificationManager.MAX_NOTIFICATIONS_TO_SHOW_KEY, 8L); + appDataStore.saveString(AppNotificationManager.TASK_NOTIFICATION_SETTINGS_KEY, "{\"maxNotifications\": 8}"); } @Test @@ -65,7 +66,7 @@ public void showsAndDismissesAllNotifications() throws JSONException { @Test public void showsOnlyMaxAllowedNotifications() throws JSONException { String jsData = "[" + getJSTaskNotificationString(startOfDay, startOfDay, startOfDay) + "]"; - appDataStore.saveLongBlocking(AppNotificationManager.MAX_NOTIFICATIONS_TO_SHOW_KEY, 1L); + appDataStore.saveStringBlocking(AppNotificationManager.TASK_NOTIFICATION_SETTINGS_KEY, "{\"maxNotifications\": 1}"); appNotificationManager.showNotificationsFromJsArray(jsData); assertEquals(1, shadowNotificationManager.getAllNotifications().size()); } @@ -87,7 +88,7 @@ public void respectsNotificationsOrderAndStoreLatestReadyAtTimestamp() throws JS } ] """.formatted(jsData, latestReadyAtTimestamp, startOfDay, startOfDay); - appDataStore.saveLongBlocking(AppNotificationManager.MAX_NOTIFICATIONS_TO_SHOW_KEY, 1L); + appDataStore.saveStringBlocking(AppNotificationManager.TASK_NOTIFICATION_SETTINGS_KEY, "{\"maxNotifications\": 1}"); appNotificationManager.showNotificationsFromJsArray(newNotificationData); assertEquals(1, shadowNotificationManager.getAllNotifications().size()); assertEquals(latestReadyAtTimestamp, appDataStore.getLongBlocking(AppNotificationManager.LATEST_NOTIFICATION_TIMESTAMP_KEY, 0L)); diff --git a/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java b/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java new file mode 100644 index 00000000..9a10d6d3 --- /dev/null +++ b/src/test/java/org/medicmobile/webapp/mobile/NotificationWorkerTest.java @@ -0,0 +1,261 @@ +package org.medicmobile.webapp.mobile; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; + +import androidx.test.core.app.ApplicationProvider; +import androidx.work.ListenableWorker; +import androidx.work.WorkerParameters; + +import org.json.JSONException; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.medicmobile.webapp.mobile.util.AppDataStore; +import org.mockito.Mock; +import org.mockito.MockedConstruction; +import org.mockito.MockedStatic; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.robolectric.RobolectricTestRunner; + +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +@RunWith(RobolectricTestRunner.class) +public class NotificationWorkerTest { + private static final LocalTime FIXED_NOW = LocalTime.of(12, 0); + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private AppDataStore mockAppDataStore; + + private Context context; + + @Before + public void setUp() { + context = ApplicationProvider.getApplicationContext(); + } + + @Test + public void doWork_returnsSuccess_whenNoException() throws JSONException { + String notificationWindowSettings = "{}"; // Empty = always in window + String notifications = "[]"; + + try (MockedStatic dataMock = mockStatic(AppDataStore.class); + MockedConstruction notificationMgrMock = mockConstruction( + AppNotificationManager.class)) { + dataMock.when(() -> AppDataStore.getInstance(context)) + .thenReturn(mockAppDataStore); + + when(mockAppDataStore.getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_SETTINGS_KEY, "{}")) + .thenReturn(notificationWindowSettings); + when(mockAppDataStore.getStringBlocking(AppNotificationManager.TASK_NOTIFICATIONS_KEY, "[]")) + .thenReturn(notifications); + + NotificationWorker worker = createWorker(); + + ListenableWorker.Result result = worker.doWork(); + + // Assert + assertEquals(ListenableWorker.Result.success(), result); + verify(notificationMgrMock.constructed().get(0), times(1)) + .showNotificationsFromJsArray(notifications); + } + } + + @Test + public void doWork_returnsFailure_whenJSONExceptionOccurs() throws JSONException { + String invalidNotificationWindowSettings = "{invalid json}"; + + try (MockedStatic dataMock = mockStatic(AppDataStore.class); + MockedConstruction notificationMgrMock = mockConstruction( + AppNotificationManager.class)) { + + dataMock.when(() -> AppDataStore.getInstance(context)) + .thenReturn(mockAppDataStore); + + when(mockAppDataStore + .getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_SETTINGS_KEY, "{}")) + .thenReturn(invalidNotificationWindowSettings); + when(mockAppDataStore + .getStringBlocking(AppNotificationManager.TASK_NOTIFICATIONS_KEY, "[]")) + .thenReturn("[]"); + + NotificationWorker worker = createWorker(); + + ListenableWorker.Result result = worker.doWork(); + + // Invalid JSON is handled gracefully + // by Utils.parseJSONObject which returns empty JSONObject + // So this returns success and showNotifications is called + assertEquals(ListenableWorker.Result.success(), result); + verify(notificationMgrMock.constructed().get(0), times(1)) + .showNotificationsFromJsArray(anyString()); + } + } + + @Test + public void doWork_callsShowNotifications_whenInNotificationWindow() throws JSONException { + String notificationWindowSettings = getWindowSettings(FIXED_NOW.minusHours(1), FIXED_NOW.plusHours(1)); + String notifications = "[]"; + + try (MockedStatic dataMock = mockStatic(AppDataStore.class); + MockedStatic timeMock = mockNow(FIXED_NOW); + MockedConstruction notificationMgrMock = mockConstruction( + AppNotificationManager.class)) { + + dataMock.when(() -> AppDataStore.getInstance(context)) + .thenReturn(mockAppDataStore); + + when(mockAppDataStore + .getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_SETTINGS_KEY, "{}")) + .thenReturn(notificationWindowSettings); + when(mockAppDataStore + .getStringBlocking(AppNotificationManager.TASK_NOTIFICATIONS_KEY, "[]")) + .thenReturn(notifications); + + NotificationWorker worker = createWorker(); + + ListenableWorker.Result result = worker.doWork(); + + assertEquals(ListenableWorker.Result.success(), result); + verify(notificationMgrMock.constructed().get(0), times(1)) + .showNotificationsFromJsArray(notifications); + } + } + + @Test + public void doWork_doesNotCallShowNotifications_whenOutsideNotificationWindow() throws JSONException { + // window earlier in the same day, "now" is past it + String notificationWindowSettings = getWindowSettings(FIXED_NOW.minusHours(2), FIXED_NOW.minusHours(1)); + + try (MockedStatic dataMock = mockStatic(AppDataStore.class); + MockedStatic timeMock = mockNow(FIXED_NOW); + MockedConstruction notificationMgrMock = mockConstruction( + AppNotificationManager.class)) { + + dataMock.when(() -> AppDataStore.getInstance(context)) + .thenReturn(mockAppDataStore); + + when(mockAppDataStore + .getStringBlocking(AppNotificationManager.TASK_NOTIFICATION_SETTINGS_KEY, "{}")) + .thenReturn(notificationWindowSettings); + + NotificationWorker worker = createWorker(); + + ListenableWorker.Result result = worker.doWork(); + + // Assert + assertEquals(ListenableWorker.Result.success(), result); + verify(notificationMgrMock.constructed().get(0), times(0)) + .showNotificationsFromJsArray(anyString()); + } + } + + // Apps with invalid window settings time don't run + @Test + public void isNotificationWindow_returnsFalse_whenBadTimeFields() throws Exception { + String windowSettings = "{\"start\": \"09:70\",\"end\": \"19:05\"}"; // bad data + + try (MockedStatic dataMock = mockStatic(AppDataStore.class)) { + + dataMock.when(() -> AppDataStore.getInstance(context)) + .thenReturn(mockAppDataStore); + + NotificationWorker worker = createWorker(); + + boolean result = worker.isNotificationWindow(Utils.parseJSONObject(windowSettings)); + assertFalse(result); + } + } + + @Test + public void isNotificationWindow_returnsTrue_whenWithinWindow() throws Exception { + String windowSettings = getWindowSettings(FIXED_NOW.minusHours(1), FIXED_NOW.plusHours(1)); + + try (MockedStatic dataMock = mockStatic(AppDataStore.class); + MockedStatic timeMock = mockNow(FIXED_NOW)) { + + dataMock.when(() -> AppDataStore.getInstance(context)) + .thenReturn(mockAppDataStore); + + NotificationWorker worker = createWorker(); + + boolean result = worker.isNotificationWindow(Utils.parseJSONObject(windowSettings)); + assertTrue(result); + } + } + + @Test + public void isNotificationWindow_returnsFalse_whenBeforeWindow() throws Exception { + String windowSettings = getWindowSettings(FIXED_NOW.plusHours(1), FIXED_NOW.plusHours(2)); + + try (MockedStatic dataMock = mockStatic(AppDataStore.class); + MockedStatic timeMock = mockNow(FIXED_NOW)) { + + dataMock.when(() -> AppDataStore.getInstance(context)) + .thenReturn(mockAppDataStore); + + NotificationWorker worker = createWorker(); + + boolean result = worker.isNotificationWindow(Utils.parseJSONObject(windowSettings)); + + // Assert + assertFalse(result); + } + } + + @Test + public void isNotificationWindow_returnsFalse_whenAfterWindow() throws Exception { + String windowSettings = getWindowSettings(FIXED_NOW.minusHours(2), FIXED_NOW.minusHours(1)); + + try (MockedStatic dataMock = mockStatic(AppDataStore.class); + MockedStatic timeMock = mockNow(FIXED_NOW)) { + + dataMock.when(() -> AppDataStore.getInstance(context)) + .thenReturn(mockAppDataStore); + + NotificationWorker worker = createWorker(); + + boolean result = worker.isNotificationWindow(Utils.parseJSONObject(windowSettings)); + + // Assert + assertFalse(result); + } + } + + private NotificationWorker createWorker() { + WorkerParameters workerParameters = mock(WorkerParameters.class); + return new NotificationWorker(context, workerParameters); + } + + private String getWindowSettings(LocalTime startTime, LocalTime endTime) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm"); + return String.format("{\"start\": \"%s\", \"end\": \"%s\"}", + startTime.format(formatter), endTime.format(formatter)); + } + + // Pins LocalTime.now(...) to passed arg + private MockedStatic mockNow(LocalTime now) { + MockedStatic timeMock = mockStatic(LocalTime.class, CALLS_REAL_METHODS); + timeMock.when(() -> LocalTime.now(any(ZoneId.class))).thenReturn(now); + return timeMock; + } +} diff --git a/src/test/java/org/medicmobile/webapp/mobile/UtilsTest.java b/src/test/java/org/medicmobile/webapp/mobile/UtilsTest.java index efe3ddf1..4f598f02 100644 --- a/src/test/java/org/medicmobile/webapp/mobile/UtilsTest.java +++ b/src/test/java/org/medicmobile/webapp/mobile/UtilsTest.java @@ -2,6 +2,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -22,6 +23,7 @@ import java.io.File; import java.io.IOException; +import java.time.LocalTime; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -38,7 +40,7 @@ public void isUrlRelated_goodNormalUrls() { "https://example.com/medic/_design/medic/_rewrite", }; - for(String goodUrl : goodUrls) { + for (String goodUrl : goodUrls) { assertTrue("Expected URL to be accepted, but it wasn't: " + goodUrl, Utils.isUrlRelated("https://example.com", Uri.parse(goodUrl))); } @@ -50,7 +52,7 @@ public void isUrlRelated_goodBlobs() { "blob:https://example.com/medic/_design/medic/_rewrite", }; - for(String goodBlobUrl : goodBlobUrls) { + for (String goodBlobUrl : goodBlobUrls) { assertTrue("Expected URL to be accepted, but it wasn't: " + goodBlobUrl, Utils.isUrlRelated("https://example.com", Uri.parse(goodBlobUrl))); } @@ -66,7 +68,7 @@ public void isUrlRelated_badUrls() { "sms:0040733898569,0040788963214&body=Thisisthesmsbody", }; - for(String badUrl : badUrls) { + for (String badUrl : badUrls) { assertFalse("Expected URL to be rejected, but it wasn't: " + badUrl, Utils.isUrlRelated("https://example.com", Uri.parse(badUrl))); } @@ -121,7 +123,7 @@ public void validNavigationUrls() { "blob:https://gamma-cht.dev.medicmobile.org/#/reports" }; - for(String goodBlobUrl : goodBlobUrls) { + for (String goodBlobUrl : goodBlobUrls) { assertTrue("Expected URL to be accepted, but it wasn't: " + goodBlobUrl, Utils.isValidNavigationUrl("https://gamma-cht.dev.medicmobile.org", goodBlobUrl)); } @@ -136,7 +138,7 @@ public void nullUrlsNotValid() { {"", ""}, }; - for(String[] nullUrlPair : nullUrls) { + for (String[] nullUrlPair : nullUrls) { assertFalse("Not expected URLs to be accepted, but they were: " + nullUrlPair[0] + " , " + nullUrlPair[1], Utils.isValidNavigationUrl(nullUrlPair[0], nullUrlPair[1])); } @@ -268,4 +270,26 @@ public void testPackageManagerException() throws PackageManager.NameNotFoundExce )); } } + + @Test + public void formatTime_returnsNullForBadTime() { + LocalTime result = Utils.formatTime("14:70"); + assertNull(result); + } + + @Test + public void formatTime_parsesValidTimeFormats() { + String[] testTimes = {"00:00", "12:00", "23:59"}; + LocalTime[] expectedTimes = { + LocalTime.of(0, 0), + LocalTime.of(12, 0), + LocalTime.of(23, 59) + }; + + //Assert + for (int i = 0; i < testTimes.length; i++) { + LocalTime result = Utils.formatTime(testTimes[i]); + assertEquals(expectedTimes[i], result); + } + } }