🚧 Add preferences helpers and tests
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sun, 6 Apr 2025 09:01:01 +0000 (11:01 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Sun, 6 Apr 2025 16:19:38 +0000 (18:19 +0200)
src/main/kotlin/net/pterodactylus/sone/core/Preferences.kt
src/test/kotlin/net/pterodactylus/sone/core/PreferencesTest.kt
src/test/kotlin/net/pterodactylus/sone/test/Mocks.kt

index dc15810..b701cc3 100644 (file)
@@ -18,6 +18,9 @@
 package net.pterodactylus.sone.core
 
 import com.google.common.eventbus.EventBus
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KMutableProperty0
+import kotlin.reflect.KProperty
 import net.pterodactylus.sone.core.event.InsertionDelayChangedEvent
 import net.pterodactylus.sone.core.event.StrictFilteringActivatedEvent
 import net.pterodactylus.sone.core.event.StrictFilteringDeactivatedEvent
@@ -145,3 +148,55 @@ interface Preferences {
 }
 
 private val unsupported: Nothing get() = throw UnsupportedOperationException()
+
+fun Preferences.withDefaults() = object : Preferences by this {
+       override var insertionDelay: Int? by default(60, this@withDefaults::insertionDelay)
+       override var postsPerPage: Int? by default(10, this@withDefaults::postsPerPage)
+       override var imagesPerPage: Int? by default(9, this@withDefaults::imagesPerPage)
+       override var charactersPerPost: Int? by default(400, this@withDefaults::charactersPerPost)
+       override var postCutOffLength: Int? by default(200, this@withDefaults::postCutOffLength)
+       override var requireFullAccess: Boolean? by default(false, this@withDefaults::requireFullAccess)
+       override var fcpInterfaceActive: Boolean? by default(false, this@withDefaults::fcpInterfaceActive)
+       override var fcpFullAccessRequired: FullAccessRequired? by default(ALWAYS, this@withDefaults::fcpFullAccessRequired)
+       override var strictFiltering: Boolean? by default(false, this@withDefaults::strictFiltering)
+}
+
+fun Preferences.validated() = object : Preferences by this {
+       override var insertionDelay: Int? by validated(this@validated::insertionDelay) { v -> (v == null) || (v >= 0) }
+       override var postsPerPage: Int? by validated(this@validated::postsPerPage) { v -> (v == null) || (v >= 1) }
+       override var imagesPerPage: Int? by validated(this@validated::imagesPerPage) { v -> (v == null) || (v >= 1) }
+       override var charactersPerPost: Int? by validated(this@validated::charactersPerPost) { v -> (v == null) || (v == -1) || (v >= 50) }
+       override var postCutOffLength: Int? by validated(this@validated::postCutOffLength) { v -> (v == null) || (v >= 50) }
+}
+
+fun Preferences.withEvents(eventBus: EventBus) = object : Preferences by this {
+       override var insertionDelay: Int? by sendEvent(this@withEvents::insertionDelay) { eventBus.post(InsertionDelayChangedEvent(it!!)) }
+       override var fcpInterfaceActive: Boolean? by sendEvent(this@withEvents::fcpInterfaceActive) { eventBus.post(if (it!!) FcpInterfaceActivatedEvent() else FcpInterfaceDeactivatedEvent()) }
+       override var fcpFullAccessRequired: FullAccessRequired? by sendEvent(this@withEvents::fcpFullAccessRequired) { eventBus.post(FullAccessRequiredChanged(it!!)) }
+       override var strictFiltering: Boolean? by sendEvent(this@withEvents::strictFiltering) { eventBus.post(if (it!!) StrictFilteringActivatedEvent() else StrictFilteringDeactivatedEvent()) }
+}
+
+private fun <V> default(defaultValue: V, parent: KMutableProperty0<V?>) = object : ReadWriteProperty<Preferences, V?> {
+       override operator fun getValue(thisRef: Preferences, property: KProperty<*>) = parent.get() ?: defaultValue
+       override operator fun setValue(thisRef: Preferences, property: KProperty<*>, value: V?) {
+               parent.set(value)
+       }
+}
+
+private fun <V> validated(parent: KMutableProperty0<V?>, validator: (V?) -> Boolean) = object : ReadWriteProperty<Preferences, V?> {
+       override fun getValue(thisRef: Preferences, property: KProperty<*>) = parent.get()
+       override operator fun setValue(thisRef: Preferences, property: KProperty<*>, value: V?) {
+               if (!validator(value)) {
+                       throw IllegalArgumentException("$value is not valid for ${property.name}")
+               }
+               parent.set(value)
+       }
+}
+
+private fun <V> sendEvent(parent: KMutableProperty0<V?>, valueConsumer: (V?) -> Unit) = object : ReadWriteProperty<Preferences, V?> {
+       override operator fun getValue(thisRef: Preferences, property: KProperty<*>) = parent.get()
+       override operator fun setValue(thisRef: Preferences, property: KProperty<*>, value: V?) {
+               parent.set(value)
+               valueConsumer(parent.get())
+       }
+}
index 37a845a..cd4d9cd 100644 (file)
@@ -2,6 +2,7 @@ package net.pterodactylus.sone.core
 
 import com.google.common.eventbus.EventBus
 import com.google.common.eventbus.Subscribe
+import java.util.concurrent.atomic.AtomicBoolean
 import net.pterodactylus.sone.core.event.InsertionDelayChangedEvent
 import net.pterodactylus.sone.core.event.StrictFilteringActivatedEvent
 import net.pterodactylus.sone.core.event.StrictFilteringDeactivatedEvent
@@ -12,10 +13,13 @@ import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.WRITING
 import net.pterodactylus.sone.fcp.event.FcpInterfaceActivatedEvent
 import net.pterodactylus.sone.fcp.event.FcpInterfaceDeactivatedEvent
 import net.pterodactylus.sone.fcp.event.FullAccessRequiredChanged
+import net.pterodactylus.sone.test.allNullPreferences
+import net.pterodactylus.sone.test.assertThrows
 import net.pterodactylus.util.config.Configuration
 import net.pterodactylus.util.config.MapConfigurationBackend
 import org.hamcrest.Matcher
 import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.contains
 import org.hamcrest.Matchers.emptyIterable
 import org.hamcrest.Matchers.equalTo
 import org.hamcrest.Matchers.hasItem
@@ -348,3 +352,282 @@ class DefaultPreferencesTest {
        }
 
 }
+
+class PreferencesTest {
+
+       @Test
+       fun `preferences with defaults return 60s post insertion delay if underlying preferences return null`() {
+               assertThat(testPreferences.withDefaults().insertionDelay, equalTo(60))
+       }
+
+       @Test
+       fun `preferences with defaults return 10 posts per page if underlying preferences return null`() {
+               assertThat(testPreferences.withDefaults().postsPerPage, equalTo(10))
+       }
+
+       @Test
+       fun `preferences with defaults return 9 images per page if underlying preferences return null`() {
+               assertThat(testPreferences.withDefaults().imagesPerPage, equalTo(9))
+       }
+
+       @Test
+       fun `preferences with defaults return 400 characters per post if underlying preferences return null`() {
+               assertThat(testPreferences.withDefaults().charactersPerPost, equalTo(400))
+       }
+
+       @Test
+       fun `preferences with defaults return a post cut off length of 200 if underlying preferences return null`() {
+               assertThat(testPreferences.withDefaults().postCutOffLength, equalTo(200))
+       }
+
+       @Test
+       fun `preferences with defaults return false for require full access if underlying preferences return null`() {
+               assertThat(testPreferences.withDefaults().requireFullAccess, equalTo(false))
+       }
+
+       @Test
+       fun `preferences with defaults return false for fcp interface active if underlying preferences return null`() {
+               assertThat(testPreferences.withDefaults().fcpInterfaceActive, equalTo(false))
+       }
+
+       @Test
+       fun `preferences with defaults return always for fcp full access required if underlying preferences return null`() {
+               assertThat(testPreferences.withDefaults().fcpFullAccessRequired, equalTo(ALWAYS))
+       }
+
+       @Test
+       fun `preferences with defaults return false for strict filtering if underlying preferences return null`() {
+               assertThat(testPreferences.withDefaults().strictFiltering, equalTo(false))
+       }
+
+       @Test
+       fun `preferences with validation refuse negative insertion delay`() {
+               assertThrows<IllegalArgumentException> { testPreferences.validated().insertionDelay = -1 }
+               assertThrows<IllegalArgumentException> { testPreferences.validated().insertionDelay = -17 }
+               assertThrows<IllegalArgumentException> { testPreferences.validated().insertionDelay = -38 }
+       }
+
+       @Test
+       fun `preferences with validation accept positive insertion delay`() {
+               testPreferences.validated().insertionDelay = 0
+               testPreferences.validated().insertionDelay = 1
+               testPreferences.validated().insertionDelay = 300
+       }
+
+       @Test
+       fun `preferences with validation accept null as insertion delay`() {
+               testPreferences.validated().insertionDelay = null
+       }
+
+       @Test
+       fun `preferences with validation refuse less than one post per page`() {
+               assertThrows<IllegalArgumentException> { testPreferences.validated().postsPerPage = 0 }
+               assertThrows<IllegalArgumentException> { testPreferences.validated().postsPerPage = -1 }
+               assertThrows<IllegalArgumentException> { testPreferences.validated().postsPerPage = -23 }
+       }
+
+       @Test
+       fun `preferences with validation accept one or more posts per page`() {
+               testPreferences.validated().postsPerPage = 1
+               testPreferences.validated().postsPerPage = 2
+               testPreferences.validated().postsPerPage = 432
+       }
+
+       @Test
+       fun `preferences with validation accept null as posts per page`() {
+               testPreferences.validated().postsPerPage = null
+       }
+
+       @Test
+       fun `preferences with validation refuse less than one image per page`() {
+               assertThrows<IllegalArgumentException> { testPreferences.validated().imagesPerPage = 0 }
+               assertThrows<IllegalArgumentException> { testPreferences.validated().imagesPerPage = -1 }
+               assertThrows<IllegalArgumentException> { testPreferences.validated().imagesPerPage = -71 }
+       }
+
+       @Test
+       fun `preferences with validation accept one or more images per page`() {
+               testPreferences.validated().imagesPerPage = 1
+               testPreferences.validated().imagesPerPage = 2
+               testPreferences.validated().imagesPerPage = 123
+       }
+
+       @Test
+       fun `preferences with validation accept null as images per page`() {
+               testPreferences.validated().imagesPerPage = null
+       }
+
+       @Test
+       fun `preferences with validation refuse less than 50 characters per post other than -1`() {
+               assertThrows<IllegalArgumentException> { testPreferences.validated().charactersPerPost = 49 }
+               assertThrows<IllegalArgumentException> { testPreferences.validated().charactersPerPost = 0 }
+               assertThrows<IllegalArgumentException> { testPreferences.validated().charactersPerPost = -2 }
+       }
+
+       @Test
+       fun `preferences with validation accept -1 or greater than or equal to 50 characters per post`() {
+               testPreferences.validated().charactersPerPost = -1
+               testPreferences.validated().charactersPerPost = 50
+               testPreferences.validated().charactersPerPost = 234
+       }
+
+       @Test
+       fun `preferences with validation accept null as characters per post`() {
+               testPreferences.validated().charactersPerPost = null
+       }
+
+       @Test
+       fun `preferences with validation refuse less than 50 characters post cut off length`() {
+               assertThrows<IllegalArgumentException> { testPreferences.validated().postCutOffLength = 49 }
+               assertThrows<IllegalArgumentException> { testPreferences.validated().postCutOffLength = 0 }
+               assertThrows<IllegalArgumentException> { testPreferences.validated().postCutOffLength = -1 }
+       }
+
+       @Test
+       fun `preferences with validation accept greater than or equal to 50 characters post cut off length`() {
+               testPreferences.validated().postCutOffLength = 50
+               testPreferences.validated().postCutOffLength = 135
+               testPreferences.validated().postCutOffLength = 234
+       }
+
+       @Test
+       fun `preferences with validation accept null as characters post cut off length`() {
+               testPreferences.validated().postCutOffLength = null
+       }
+
+       @Test
+       fun `preferences with validation allows all booleans for require full access`() {
+               testPreferences.validated().requireFullAccess = false
+               testPreferences.validated().requireFullAccess = true
+       }
+
+       @Test
+       fun `preferences with validation accept null as require full access`() {
+               testPreferences.validated().requireFullAccess = null
+       }
+
+       @Test
+       fun `preferences with validation allows all booleans for fcp interface active`() {
+               testPreferences.validated().fcpInterfaceActive = false
+               testPreferences.validated().fcpInterfaceActive = true
+       }
+
+       @Test
+       fun `preferences with validation accept null as fcp interface active`() {
+               testPreferences.validated().fcpInterfaceActive = null
+       }
+
+       @Test
+       fun `preferences with validation allows all enum values for fcp full access required`() {
+               testPreferences.validated().fcpFullAccessRequired = NO
+               testPreferences.validated().fcpFullAccessRequired = WRITING
+               testPreferences.validated().fcpFullAccessRequired = ALWAYS
+       }
+
+       @Test
+       fun `preferences with validation accept null as fcp full access required`() {
+               testPreferences.validated().fcpFullAccessRequired = null
+       }
+
+       @Test
+       fun `preferences with validation allows all booleans for strict filtering`() {
+               testPreferences.validated().strictFiltering = false
+               testPreferences.validated().strictFiltering = true
+       }
+
+       @Test
+       fun `preferences with validation accept null as strict filtering`() {
+               testPreferences.validated().strictFiltering = null
+       }
+
+       @Test
+       fun `preferences with events send insertion delay changed event when insertion delay is changed`() {
+               val newInsertionDelay = mutableListOf<Int?>()
+               eventBus.register(object {
+                       @Subscribe
+                       fun onInsertionDelayChanged(event: InsertionDelayChangedEvent) {
+                               newInsertionDelay += event.insertionDelay
+                       }
+               })
+               testPreferences.withEvents(eventBus).insertionDelay = 123
+               assertThat(newInsertionDelay, contains(123))
+       }
+
+       @Test
+       fun `preferences with defaults and events send default insertion delay changed event when insertion delay is changed to null`() {
+               val newInsertionDelay = mutableListOf<Int?>()
+               eventBus.register(object {
+                       @Subscribe
+                       fun onInsertionDelayChanged(event: InsertionDelayChangedEvent) {
+                               newInsertionDelay += event.insertionDelay
+                       }
+               })
+               testPreferences.withDefaults().withEvents(eventBus).insertionDelay = null
+               assertThat(newInsertionDelay, contains(60))
+       }
+
+       @Test
+       fun `preferences with events send fcp interface activated event when fcp interface active is set to true`() {
+               val called = AtomicBoolean(false)
+               eventBus.register(object {
+                       @Subscribe
+                       fun onFcpInterfaceActivated(event: FcpInterfaceActivatedEvent) = called.set(true)
+               })
+               testPreferences.withEvents(eventBus).fcpInterfaceActive = true
+               assertThat(called.get(), equalTo(true))
+       }
+
+       @Test
+       fun `preferences with events send fcp interface deactivated event when fcp interface active is set to false`() {
+               val called = AtomicBoolean(false)
+               eventBus.register(object {
+                       @Subscribe
+                       fun onFcpInterfaceDeactivated(event: FcpInterfaceDeactivatedEvent) = called.set(true)
+               })
+               testPreferences.withEvents(eventBus).fcpInterfaceActive = false
+               assertThat(called.get(), equalTo(true))
+       }
+
+       @Test
+       fun `preferences with events send fcp full access required changed event when fcp full access required is changed`() {
+               val fcpFullAccessRequiredValues = mutableListOf<FullAccessRequired>()
+               eventBus.register(object {
+                       @Subscribe
+                       fun onFcpFullAccessRequiredChanged(event: FullAccessRequiredChanged) {
+                               fcpFullAccessRequiredValues += event.fullAccessRequired
+                       }
+               })
+               testPreferences.withEvents(eventBus).apply {
+                       fcpFullAccessRequired = WRITING
+                       fcpFullAccessRequired = ALWAYS
+                       fcpFullAccessRequired = NO
+               }
+               assertThat(fcpFullAccessRequiredValues, contains(WRITING, ALWAYS, NO))
+       }
+
+       @Test
+       fun `preferences with events send strict filtering activated event when strict filtering is set to true`() {
+               val called = AtomicBoolean(false)
+               eventBus.register(object {
+                       @Subscribe
+                       fun onStrictFilteringActivated(event: StrictFilteringActivatedEvent) = called.set(true)
+               })
+               testPreferences.withEvents(eventBus).strictFiltering = true
+               assertThat(called.get(), equalTo(true))
+       }
+
+       @Test
+       fun `preferences with events send strict filtering deactivated event when strict filtering is set to false`() {
+               val called = AtomicBoolean(false)
+               eventBus.register(object {
+                       @Subscribe
+                       fun onStrictFilteringDeactivated(event: StrictFilteringDeactivatedEvent) = called.set(true)
+               })
+               testPreferences.withEvents(eventBus).strictFiltering = false
+               assertThat(called.get(), equalTo(true))
+       }
+
+       private val eventBus = EventBus()
+       private val testPreferences = allNullPreferences()
+
+}
index 54307a8..cf6532b 100644 (file)
@@ -35,6 +35,10 @@ import net.pterodactylus.sone.freenet.wot.OwnIdentity
 import net.pterodactylus.sone.utils.asFreenetBase64
 import net.pterodactylus.sone.utils.asOptional
 import java.util.UUID
+import net.pterodactylus.sone.core.Preferences
+import net.pterodactylus.sone.core.validated
+import net.pterodactylus.sone.core.withDefaults
+import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired
 
 val remoteSone1 = createRemoteSone()
 val remoteSone2 = createRemoteSone()
@@ -96,3 +100,17 @@ fun createPostReply(text: String = "text", post: Post? = createPost(), sone: Son
 
 fun createImage(sone: Sone): Image =
                ImageImpl().modify().setSone(sone).update()
+
+fun allNullPreferences() = object : Preferences {
+       override var insertionDelay: Int? = null
+       override var postsPerPage: Int? = null
+       override var imagesPerPage: Int? = null
+       override var charactersPerPost: Int? = null
+       override var postCutOffLength: Int? = null
+       override var requireFullAccess: Boolean? = null
+       override var fcpInterfaceActive: Boolean? = null
+       override var fcpFullAccessRequired: FullAccessRequired? = null
+       override var strictFiltering: Boolean? = null
+}
+
+fun testPreferences() = allNullPreferences().withDefaults().validated()