From 45767453f79353b8870515fb52e036d7d2dbe0b0 Mon Sep 17 00:00:00 2001 From: Armin Date: Sat, 23 Mar 2024 15:42:32 +0100 Subject: [PATCH] Add MockSynchronizationSettings to fix duplicate DataStore file access in tests --- README.adoc | 2 +- ...CapturingServiceWithoutPermissionTest.java | 8 +- .../datacapturing/DataCapturingServiceTest.kt | 8 +- .../MockSynchronizationSettings.kt | 27 ++++ .../de/cyface/datacapturing/PingPongTest.kt | 8 +- .../settings/SynchronizationSettingsTest.kt | 6 +- .../DefaultSynchronizationSettings.kt | 122 ++++++++++++++++++ .../settings/SynchronizationSettings.kt | 106 +-------------- 8 files changed, 166 insertions(+), 121 deletions(-) create mode 100644 datacapturing/src/androidTest/kotlin/de/cyface/datacapturing/MockSynchronizationSettings.kt create mode 100644 synchronization/src/main/kotlin/de/cyface/synchronization/settings/DefaultSynchronizationSettings.kt diff --git a/README.adoc b/README.adoc index 5374bfc7a..9c4a068ae 100644 --- a/README.adoc +++ b/README.adoc @@ -433,7 +433,7 @@ class CustomApplication : Application() { // Initialize DataStore once for all settings appSettings = lazyAppSettings TrackingSettings.initialize(this) // energy_settings - CyfaceAuthenticator.settings = SynchronizationSettings( // synchronization + CyfaceAuthenticator.settings = DefaultSynchronizationSettings( // synchronization this, "https://example.com/api/v4", // Set the Data Collector URL // Movebis variant can replace oauth config with `JsonObject()` diff --git a/datacapturing/src/androidTest/java/de/cyface/datacapturing/DataCapturingServiceWithoutPermissionTest.java b/datacapturing/src/androidTest/java/de/cyface/datacapturing/DataCapturingServiceWithoutPermissionTest.java index c662dab20..e357bab3d 100644 --- a/datacapturing/src/androidTest/java/de/cyface/datacapturing/DataCapturingServiceWithoutPermissionTest.java +++ b/datacapturing/src/androidTest/java/de/cyface/datacapturing/DataCapturingServiceWithoutPermissionTest.java @@ -48,7 +48,7 @@ import de.cyface.datacapturing.ui.UIListener; import de.cyface.persistence.model.Modality; import de.cyface.synchronization.CyfaceAuthenticator; -import de.cyface.synchronization.settings.SynchronizationSettings; +import de.cyface.synchronization.settings.DefaultSynchronizationSettings; /** * Checks if missing permissions are correctly detected before starting a service. @@ -90,11 +90,7 @@ public void setUp() throws JSONException { // The LOGIN_ACTIVITY is normally set to the LoginActivity of the SDK implementing app CyfaceAuthenticator.LOGIN_ACTIVITY = AccountAuthenticatorActivity.class; - CyfaceAuthenticator.settings = new SynchronizationSettings( - context, - "https://TEST_URL/", - new JSONObject().put("discovery_uri", "https://TEST_URL/") - ); + CyfaceAuthenticator.settings = new MockSynchronizationSettings(); //final String dataUploadServerAddress = "https://localhost:8080/api/v3"; final DataCapturingListener listener = new TestListener(); diff --git a/datacapturing/src/androidTest/kotlin/de/cyface/datacapturing/DataCapturingServiceTest.kt b/datacapturing/src/androidTest/kotlin/de/cyface/datacapturing/DataCapturingServiceTest.kt index bd26eac46..96324c001 100644 --- a/datacapturing/src/androidTest/kotlin/de/cyface/datacapturing/DataCapturingServiceTest.kt +++ b/datacapturing/src/androidTest/kotlin/de/cyface/datacapturing/DataCapturingServiceTest.kt @@ -46,7 +46,7 @@ import de.cyface.persistence.model.Measurement import de.cyface.persistence.model.MeasurementStatus import de.cyface.persistence.model.Modality import de.cyface.synchronization.CyfaceAuthenticator -import de.cyface.synchronization.settings.SynchronizationSettings +import de.cyface.synchronization.settings.DefaultSynchronizationSettings import de.cyface.testutils.SharedTestUtils.clearPersistenceLayer import de.cyface.utils.Validate import kotlinx.coroutines.runBlocking @@ -122,11 +122,7 @@ class DataCapturingServiceTest { // The LOGIN_ACTIVITY is normally set to the LoginActivity of the SDK implementing app CyfaceAuthenticator.LOGIN_ACTIVITY = Activity::class.java - CyfaceAuthenticator.settings = SynchronizationSettings( - context!!, - "https://TEST_URL/", - JSONObject().put("discovery_uri", "https://TEST_URL/") - ) + CyfaceAuthenticator.settings = MockSynchronizationSettings() // Add test account val requestAccount = Account(TestUtils.DEFAULT_USERNAME, TestUtils.ACCOUNT_TYPE) diff --git a/datacapturing/src/androidTest/kotlin/de/cyface/datacapturing/MockSynchronizationSettings.kt b/datacapturing/src/androidTest/kotlin/de/cyface/datacapturing/MockSynchronizationSettings.kt new file mode 100644 index 000000000..fc59df672 --- /dev/null +++ b/datacapturing/src/androidTest/kotlin/de/cyface/datacapturing/MockSynchronizationSettings.kt @@ -0,0 +1,27 @@ +package de.cyface.datacapturing + +import de.cyface.synchronization.settings.SynchronizationSettings +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.json.JSONObject + +class MockSynchronizationSettings : SynchronizationSettings { + + private var _collectorUrl = MutableStateFlow("https://TEST_URL/") + private var _oAuthConfig = MutableStateFlow(JSONObject().put("discovery_uri", "https://TEST_URL/").toString()) + + override suspend fun setCollectorUrl(value: String) { + _collectorUrl.value = value + } + + override val collectorUrlFlow: Flow + get() = _collectorUrl.asStateFlow() + + override suspend fun setOAuthConfiguration(value: String) { + _oAuthConfig.value = value + } + + override val oAuthConfigurationFlow: Flow + get() = _oAuthConfig.asStateFlow() +} diff --git a/datacapturing/src/androidTest/kotlin/de/cyface/datacapturing/PingPongTest.kt b/datacapturing/src/androidTest/kotlin/de/cyface/datacapturing/PingPongTest.kt index f0949ed82..c1e7b45dd 100644 --- a/datacapturing/src/androidTest/kotlin/de/cyface/datacapturing/PingPongTest.kt +++ b/datacapturing/src/androidTest/kotlin/de/cyface/datacapturing/PingPongTest.kt @@ -37,7 +37,7 @@ import de.cyface.persistence.SetupException import de.cyface.persistence.exception.NoSuchMeasurementException import de.cyface.persistence.model.Modality import de.cyface.synchronization.CyfaceAuthenticator -import de.cyface.synchronization.settings.SynchronizationSettings +import de.cyface.synchronization.settings.DefaultSynchronizationSettings import de.cyface.testutils.SharedTestUtils.clearPersistenceLayer import kotlinx.coroutines.runBlocking import org.hamcrest.CoreMatchers @@ -142,11 +142,7 @@ class PingPongTest { val testListener: DataCapturingListener = TestListener() // The LOGIN_ACTIVITY is normally set to the LoginActivity of the SDK implementing app CyfaceAuthenticator.LOGIN_ACTIVITY = Activity::class.java - CyfaceAuthenticator.settings = SynchronizationSettings( - context!!, - "https://TEST_URL/", - JSONObject().put("discovery_uri", "https://TEST_URL/") - ) + CyfaceAuthenticator.settings = MockSynchronizationSettings() InstrumentationRegistry.getInstrumentation().runOnMainSync { dcs = try { CyfaceDataCapturingService( diff --git a/synchronization/src/androidTest/kotlin/de/cyface/synchronization/settings/SynchronizationSettingsTest.kt b/synchronization/src/androidTest/kotlin/de/cyface/synchronization/settings/SynchronizationSettingsTest.kt index 506da9c96..2688a8445 100644 --- a/synchronization/src/androidTest/kotlin/de/cyface/synchronization/settings/SynchronizationSettingsTest.kt +++ b/synchronization/src/androidTest/kotlin/de/cyface/synchronization/settings/SynchronizationSettingsTest.kt @@ -26,7 +26,7 @@ import org.junit.Before import org.junit.Test /** - * Test the inner workings of the [SynchronizationSettings]. + * Test the inner workings of the [DefaultSynchronizationSettings]. * * @author Armin Schnabel * @version 1.0.0 @@ -42,7 +42,7 @@ class SynchronizationSettingsTest { } /** - * Test that checks that the [SynchronizationSettings] constructor only accepts API URls with + * Test that checks that the [DefaultSynchronizationSettings] constructor only accepts API URls with * "https://" as protocol. * * We had twice the problem that SDK implementors used no or a false protocol. This test ensures @@ -52,7 +52,7 @@ class SynchronizationSettingsTest { @Test(expected = SetupException::class) @Throws(SetupException::class) fun testConstructor_doesNotAcceptUrlWithoutProtocol() { - SynchronizationSettings( + DefaultSynchronizationSettings( context!!, "localhost:8080/api/v3", JSONObject() diff --git a/synchronization/src/main/kotlin/de/cyface/synchronization/settings/DefaultSynchronizationSettings.kt b/synchronization/src/main/kotlin/de/cyface/synchronization/settings/DefaultSynchronizationSettings.kt new file mode 100644 index 000000000..580689dc0 --- /dev/null +++ b/synchronization/src/main/kotlin/de/cyface/synchronization/settings/DefaultSynchronizationSettings.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2023 Cyface GmbH + * + * This file is part of the Cyface SDK for Android. + * + * The Cyface SDK for Android is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Cyface SDK for Android is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with the Cyface SDK for Android. If not, see . + */ +package de.cyface.synchronization.settings + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.core.MultiProcessDataStoreFactory +import de.cyface.persistence.SetupException +import de.cyface.synchronization.Settings +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import org.json.JSONObject +import java.io.File + +/** + * Settings used by this library. + * + * @author Armin Schnabel + * @version 2.0.0 + * @since 7.8.1 + */ +class DefaultSynchronizationSettings(context: Context, collectorUrl: String, oAuthConfig: JSONObject) : SynchronizationSettings { + + /** + * This avoids leaking the context when this object outlives the Activity of Fragment. + */ + private val appContext = context.applicationContext + + /** + * The data store with multi-process support. + * + * The reason for multi-process support is that some settings are accessed by a background + * service which runs in another process. + * + * Attention: + * - Never mix SingleProcessDataStore with MultiProcessDataStore for the same file. + * - We use MultiProcessDataStore, i.e. the preferences can be accessed from multiple processes. + * - Only create one instance of `DataStore` per file in the same process. + * - We use ProtoBuf to ensure type safety. Rebuild after changing the .proto file. + */ + private val dataStore: DataStore = MultiProcessDataStoreFactory.create( + serializer = SettingsSerializer, + produceFile = { + // With cacheDir the settings are lost on app restart [RFR-799] + File("${appContext.filesDir.path}/synchronization.pb") + }, + migrations = listOf( + PreferencesMigrationFactory.create(appContext, collectorUrl, oAuthConfig), + StoreMigration(collectorUrl, oAuthConfig) + ) + ) + + init { + if (!collectorUrl.startsWith("https://") && !collectorUrl.startsWith("http://")) { + throw SetupException("Invalid URL protocol") + } + if (!oAuthConfig.getString("discovery_uri") + .startsWith("https://") && !oAuthConfig.getString("discovery_uri") + .startsWith("http://") + ) { + throw SetupException("Invalid URL protocol") + } + } + + /** + * Sets the URL of the server to upload data to. + * + * @param value The URL to save. + */ + override suspend fun setCollectorUrl(value: String) { + dataStore.updateData { currentSettings -> + currentSettings.toBuilder() + .setCollectorUrl(value) + .build() + } + } + + /** + * @return The URL of the server to upload data to. + */ + override val collectorUrlFlow: Flow = dataStore.data + .map { settings -> + settings.collectorUrl + } + + /** + * Sets the OAuth configuration JsonObject as String. + * + * @param value The configuration to save. + */ + override suspend fun setOAuthConfiguration(value: String) { + dataStore.updateData { currentSettings -> + currentSettings.toBuilder() + .setOAuthConfiguration(value) + .build() + } + } + + /** + * @return The OAuth configuration JsonObject as String. + */ + override val oAuthConfigurationFlow: Flow = dataStore.data + .map { settings -> + settings.oAuthConfiguration + } +} \ No newline at end of file diff --git a/synchronization/src/main/kotlin/de/cyface/synchronization/settings/SynchronizationSettings.kt b/synchronization/src/main/kotlin/de/cyface/synchronization/settings/SynchronizationSettings.kt index 2bbedc602..a36af3a48 100644 --- a/synchronization/src/main/kotlin/de/cyface/synchronization/settings/SynchronizationSettings.kt +++ b/synchronization/src/main/kotlin/de/cyface/synchronization/settings/SynchronizationSettings.kt @@ -1,122 +1,30 @@ -/* - * Copyright 2023 Cyface GmbH - * - * This file is part of the Cyface SDK for Android. - * - * The Cyface SDK for Android is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * The Cyface SDK for Android is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with the Cyface SDK for Android. If not, see . - */ package de.cyface.synchronization.settings -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.core.MultiProcessDataStoreFactory -import de.cyface.persistence.SetupException -import de.cyface.synchronization.Settings import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import org.json.JSONObject -import java.io.File -/** - * Settings used by this library. - * - * @author Armin Schnabel - * @version 2.0.0 - * @since 7.8.1 - */ -class SynchronizationSettings(context: Context, collectorUrl: String, oAuthConfig: JSONObject) { - - /** - * This avoids leaking the context when this object outlives the Activity of Fragment. - */ - private val appContext = context.applicationContext - - /** - * The data store with multi-process support. - * - * The reason for multi-process support is that some settings are accessed by a background - * service which runs in another process. - * - * Attention: - * - Never mix SingleProcessDataStore with MultiProcessDataStore for the same file. - * - We use MultiProcessDataStore, i.e. the preferences can be accessed from multiple processes. - * - Only create one instance of `DataStore` per file in the same process. - * - We use ProtoBuf to ensure type safety. Rebuild after changing the .proto file. - */ - private val dataStore: DataStore = MultiProcessDataStoreFactory.create( - serializer = SettingsSerializer, - produceFile = { - // With cacheDir the settings are lost on app restart [RFR-799] - File("${appContext.filesDir.path}/synchronization.pb") - }, - migrations = listOf( - PreferencesMigrationFactory.create(appContext, collectorUrl, oAuthConfig), - StoreMigration(collectorUrl, oAuthConfig) - ) - ) - - init { - if (!collectorUrl.startsWith("https://") && !collectorUrl.startsWith("http://")) { - throw SetupException("Invalid URL protocol") - } - if (!oAuthConfig.getString("discovery_uri") - .startsWith("https://") && !oAuthConfig.getString("discovery_uri") - .startsWith("http://") - ) { - throw SetupException("Invalid URL protocol") - } - } +interface SynchronizationSettings { /** * Sets the URL of the server to upload data to. * * @param value The URL to save. */ - suspend fun setCollectorUrl(value: String) { - dataStore.updateData { currentSettings -> - currentSettings.toBuilder() - .setCollectorUrl(value) - .build() - } - } + suspend fun setCollectorUrl(value: String) /** - * @return The URL of the server to upload data to. + * @return The URL of the server to upload data to as a Kotlin Flow. */ - val collectorUrlFlow: Flow = dataStore.data - .map { settings -> - settings.collectorUrl - } + val collectorUrlFlow: Flow /** * Sets the OAuth configuration JsonObject as String. * * @param value The configuration to save. */ - suspend fun setOAuthConfiguration(value: String) { - dataStore.updateData { currentSettings -> - currentSettings.toBuilder() - .setOAuthConfiguration(value) - .build() - } - } + suspend fun setOAuthConfiguration(value: String) /** - * @return The OAuth configuration JsonObject as String. + * @return The OAuth configuration JsonObject as String as a Kotlin Flow. */ - val oAuthConfigurationFlow: Flow = dataStore.data - .map { settings -> - settings.oAuthConfiguration - } + val oAuthConfigurationFlow: Flow } \ No newline at end of file