Skip to content

Commit

Permalink
Add MockSynchronizationSettings to fix duplicate DataStore file acces…
Browse files Browse the repository at this point in the history
…s in tests
  • Loading branch information
hb0 committed Mar 23, 2024
1 parent 6f6d2c0 commit 4576745
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 121 deletions.
2 changes: 1 addition & 1 deletion README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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()`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String>
get() = _collectorUrl.asStateFlow()

override suspend fun setOAuthConfiguration(value: String) {
_oAuthConfig.value = value
}

override val oAuthConfigurationFlow: Flow<String>
get() = _oAuthConfig.asStateFlow()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -52,7 +52,7 @@ class SynchronizationSettingsTest {
@Test(expected = SetupException::class)
@Throws(SetupException::class)
fun testConstructor_doesNotAcceptUrlWithoutProtocol() {
SynchronizationSettings(
DefaultSynchronizationSettings(
context!!,
"localhost:8080/api/v3",
JSONObject()
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<Settings> = 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<String> = 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<String> = dataStore.data
.map { settings ->
settings.oAuthConfiguration
}
}
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/
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<Settings> = 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<String> = dataStore.data
.map { settings ->
settings.collectorUrl
}
val collectorUrlFlow: Flow<String>

/**
* 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<String> = dataStore.data
.map { settings ->
settings.oAuthConfiguration
}
val oAuthConfigurationFlow: Flow<String>
}

0 comments on commit 4576745

Please sign in to comment.